Merge branch 'master' into issue-191

This commit is contained in:
Gabriela Rodríguez Berón
2017-01-06 13:33:25 -03:00
committed by GitHub
56 changed files with 1045 additions and 663 deletions
+25 -7
View File
@@ -7,7 +7,7 @@ const passport = require('./services/passport');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('./services/redis');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const app = express();
@@ -43,6 +43,7 @@ const session_opts = {
rolling: true,
saveUninitialized: false,
resave: false,
unset: 'destroy',
name: 'talk.sid',
cookie: {
secure: false,
@@ -66,12 +67,6 @@ if (app.get('env') === 'production') {
app.use(session(session_opts));
//==============================================================================
// CSRF MIDDLEWARE
//==============================================================================
app.use(cookieParser());
//==============================================================================
// PASSPORT MIDDLEWARE
//==============================================================================
@@ -80,6 +75,29 @@ app.use(cookieParser());
app.use(passport.initialize());
app.use(passport.session());
//==============================================================================
// CSRF MIDDLEWARE
//==============================================================================
if (process.env.TEST_MODE === 'unit') {
// Add this fake test token in the event we are in unit test mode, and don't
// include the CSRF protection.
app.locals.csrfToken = 'UNIT_TESTS';
} else {
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
app.use(csrf({}));
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
}
//==============================================================================
// ROUTES
//==============================================================================
+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()
]);
}
+8 -3
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');
@@ -12,7 +14,7 @@ const util = require('../util');
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.TALK_PORT || (process.env.NODE_ENV === 'test' ? '3011' : '3000'));
const port = normalizePort(process.env.TALK_PORT || '3000');
app.set('port', port);
@@ -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()
]);
+10 -6
View File
@@ -80,12 +80,16 @@ function createUser(options) {
.then((user) => {
console.log(`Created user ${user.id}.`);
return User
.addRoleToUser(user.id, result.role.trim())
.then(() => {
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
util.shutdown();
});
if (result.role && result.role.length > 0) {
return User
.addRoleToUser(user.id, result.role.trim())
.then(() => {
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
util.shutdown();
});
} else {
util.shutdown();
}
})
.catch((err) => {
console.error(err);
-7
View File
@@ -11,19 +11,12 @@ export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
coralApi('/auth')
.then(result => {
if (result.csrfToken !== null) {
dispatch(check_csrf(result.csrfToken));
}
const isAdmin = !!result.user.roles.filter(i => i === 'admin').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
.catch(error => dispatch(checkLoginFailure(error)));
};
// Set CSRF Token
export const check_csrf = (_csrf) => ({type: actions.CHECK_CSRF_TOKEN, _csrf});
// LogOut Actions
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
+2 -3
View File
@@ -34,9 +34,8 @@ export const fetchModerationQueueComments = () => {
// Create a new comment
export const createComment = (name, body) => {
return (dispatch, getState) => {
const _csrf = getState().auth.get('_csrf');
const formData = {body, name, _csrf};
return (dispatch) => {
const formData = {body, name};
return coralApi('/comments', {method: 'POST', body: formData})
.then(res => dispatch({type: commentTypes.COMMENT_CREATE_SUCCESS, comment: res}))
.catch(error => dispatch({type: commentTypes.COMMENT_CREATE_FAILED, error}));
+4 -6
View File
@@ -41,18 +41,16 @@ export const newPage = () => ({
type: COMMENTERS_NEW_PAGE
});
export const setRole = (id, role) => (dispatch, getState) => {
export const setRole = (id, role) => (dispatch) => {
const _csrf = getState().auth.get('_csrf');
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}, _csrf: _csrf})
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
};
export const setCommenterStatus = (id, status) => (dispatch, getState) => {
const _csrf = getState().auth.get('_csrf');
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}, _csrf: _csrf})
export const setCommenterStatus = (id, status) => (dispatch) => {
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}})
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
+2 -3
View File
@@ -6,10 +6,9 @@ import * as actions from '../constants/user';
*/
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch, getState) => {
return (dispatch) => {
dispatch({type: actions.UPDATE_STATUS_REQUEST});
const _csrf = getState().auth.get('_csrf');
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}, _csrf})
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res}))
.catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error}));
};
@@ -7,13 +7,13 @@ import {Button} from 'react-mdl';
export default class CommentBox extends React.Component {
constructor (props) {
super(props);
this.state = {name: '', body: '', _csrf: props._csrf};
this.state = {name: '', body: ''};
this.onSubmit = this.onSubmit.bind(this);
}
onSubmit () {
const {name, body, _csrf} = this.state;
this.props.onSubmit({name, body, _csrf});
const {name, body} = this.state;
this.props.onSubmit({name, body});
this.setState({body: '', name: ''});
}
+1 -5
View File
@@ -4,8 +4,7 @@ import * as actions from '../constants/auth';
const initialState = Map({
loggedIn: false,
user: null,
isAdmin: false,
_csrf: ''
isAdmin: false
});
export default function auth (state = initialState, action) {
@@ -26,9 +25,6 @@ export default function auth (state = initialState, action) {
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
return initialState;
case actions.CHECK_CSRF_TOKEN:
return state
.set('_csrf', action._csrf);
default :
return state;
}
+6 -14
View File
@@ -23,10 +23,9 @@ 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});
export const fetchSignIn = (formData) => (dispatch, getState) => {
export const fetchSignIn = (formData) => (dispatch) => {
dispatch(signInRequest());
const _csrf = getState().auth.get('_csrf');
coralApi('/auth/local', {method: 'POST', body: formData, _csrf})
coralApi('/auth/local', {method: 'POST', body: formData})
.then(({user}) => {
const isAdmin = !!user.roles.filter(i => i === 'admin').length;
dispatch(signInSuccess(user, isAdmin));
@@ -73,11 +72,10 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
export const fetchSignUp = formData => (dispatch, getState) => {
export const fetchSignUp = formData => (dispatch) => {
dispatch(signUpRequest());
const _csrf = getState().auth.get('_csrf');
coralApi('/users', {method: 'POST', body: formData, _csrf})
coralApi('/users', {method: 'POST', body: formData})
.then(({user}) => {
dispatch(signUpSuccess(user));
setTimeout(() =>{
@@ -93,11 +91,9 @@ const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUE
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
export const fetchForgotPassword = email => (dispatch, getState) => {
export const fetchForgotPassword = email => (dispatch) => {
dispatch(forgotPassowordRequest(email));
const _csrf = getState().auth.get('_csrf');
coralApi('/users/request-password-reset', {method: 'POST', body: {email}, _csrf})
coralApi('/account/password/reset', {method: 'POST', body: {email}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
};
@@ -125,15 +121,11 @@ export const invalidForm = error => ({type: actions.INVALID_FORM, error});
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
const checkCSRF = (_csrf) => ({type: actions.CHECK_CSRF_TOKEN, _csrf});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
coralApi('/auth')
.then((result) => {
if (result.csrfToken !== null) {
dispatch(checkCSRF(result.csrfToken));
}
if (!result.user) {
throw new Error('Not logged in');
}
+2 -4
View File
@@ -190,11 +190,10 @@ export function getItemsArray (ids) {
*/
export function postItem (item, type, id) {
return (dispatch, getState) => {
return (dispatch) => {
if (id) {
item.id = id;
}
item._csrf = getState().auth.get('_csrf');
return coralApi(`/${type}`, {method: 'POST', body: item})
.then((json) => {
dispatch(addItem({...item, id:json.id}, type));
@@ -221,8 +220,7 @@ export function postItem (item, type, id) {
*/
export function postAction (item_id, item_type, action) {
return (dispatch, getState) => {
action._csrf = getState().auth.get('_csrf');
return (dispatch) => {
return coralApi(`/${item_type}/${item_id}/actions`, {method: 'POST', body: action})
.then((json) => {
dispatch(updateItem(action.item_id, action.action_type, action.id, item_type));
+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/settings', {method: 'PUT', body: formData})
.then(() => {
dispatch(addNotification('success', lang.t('successBioUpdate')));
dispatch(saveBioSuccess(settings));
dispatch(saveBioSuccess(formData));
})
.catch(error => dispatch(saveBioFailure(error)));
};
+13 -4
View File
@@ -2,19 +2,28 @@ export const base = '/api/v1';
const buildOptions = (inputOptions = {}) => {
const csurfDOM = document.head.querySelector('[property=csrf]');
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'same-origin'
credentials: 'same-origin',
_csrf: csurfDOM ? csurfDOM.content : false
};
const options = Object.assign({}, defaultOptions, inputOptions);
// Add CSRF field to each POST.
if (options.method.toLowerCase() === 'post' && options._csrf) {
options.body._csrf = options._csrf;
if (options._csrf) {
switch (options.method.toLowerCase()) {
case 'post':
case 'put':
case 'delete':
options.headers['x-csrf-token'] = options._csrf;
break;
}
}
if (options.method.toLowerCase() !== 'get') {
+1 -2
View File
@@ -11,8 +11,7 @@ const initialState = Map({
error: '',
passwordRequestSuccess: null,
passwordRequestFailure: null,
successSignUp: false,
_csrf: ''
successSignUp: false
});
const purge = user => {
+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',
+7 -17
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: {
@@ -98,7 +101,8 @@ SettingSchema.method('filterForUser', function(user = false) {
'closeTimeout',
'closedMessage',
'charCountEnable',
'charCount'
'charCount',
'requireEmailConfirmation'
]);
}
@@ -121,19 +125,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 +142,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);
});
/**
+159 -64
View File
@@ -4,9 +4,11 @@ const _ = require('lodash');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const Action = require('./action');
const Comment = require('./comment');
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
// through during the salting process.
const SALT_ROUNDS = 10;
@@ -31,6 +33,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 +93,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,47 +513,63 @@ 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',
subject: PASSWORD_RESET_JWT_SUBJECT
});
});
};
/**
* verifies a jwt and returns the associated user
* @param {String} token the JSON Web Token to verify
* Verifies that the token was indeed signed by the session secret.
* @param {String} token JWT token from the client
* @return {Promise}
*/
UserService.verifyPasswordResetToken = token => {
UserService.verifyToken = (token, options = {}) => {
return new Promise((resolve, reject) => {
jwt.verify(token, process.env.TALK_SESSION_SECRET, (error, decoded) => {
if (error) {
return reject(error);
// Set the allowed algorithms.
options.algorithms = ['HS256'];
jwt.verify(token, process.env.TALK_SESSION_SECRET, options, (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);
});
};
/**
* Verifies a jwt and returns the associated user.
* @param {String} token the JSON Web Token to verify
*/
UserService.verifyPasswordResetToken = (token) => {
return UserService
.verifyToken(token, {
subject: PASSWORD_RESET_JWT_SUBJECT
})
.then((decoded) => UserService.findById(decoded.userId));
};
/**
* Finds a user using a value which gets compared using a prefix match against
* the user's email address and/or their display name.
@@ -578,34 +608,25 @@ UserService.search = (value) => {
* Returns a count of the current users.
* @return {Promise}
*/
UserService.count = () => {
return UserModel.count();
};
UserService.count = () => UserModel.count();
/**
* Returns all the users.
* @return {Promise}
*/
UserService.all = () => {
return UserModel.find();
};
UserService.all = () => UserModel.find();
/**
* Adds a new User bio
* Updates the user's settings.
* @return {Promise}
*/
UserService.addBio = (id, bio) => (
UserModel.findOneAndUpdate({
id
}, {
$set: {
'settings.bio': bio
}
}, {
new: true
})
);
UserService.updateSettings = (id, settings) => UserModel.update({
id
}, {
$set: {
settings
}
});
/**
* Add an action to the user.
@@ -621,3 +642,77 @@ UserService.addAction = (item_id, user_id, action_type, metadata) => Action.inse
action_type,
metadata
});
/**
* This creates a token based around confirming the local profile.
* @param {String} userID The user id for the user that we are creating the
* token for.
* @param {String} email The email that we are needing to get confirmed.
* @return {Promise}
*/
UserService.createEmailConfirmToken = (userID, email) => {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
}
email = email.toLowerCase();
return UserService
.findById(userID)
.then((user) => {
if (!user) {
return Promise.reject(new Error('user not found'));
}
// 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'));
}
const payload = {
email,
userID
};
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
jwtid: uuid.v4(),
algorithm: 'HS256',
expiresIn: '1d',
subject: EMAIL_CONFIRM_JWT_SUBJECT
});
});
};
/**
* This verifies that a given token was for the email confirmation and updates
* that user's profile with a 'confirmed_at' parameter with the current date.
* @param {String} token the token containing the email confirmation details
* signed with our secret.
* @return {Promise}
*/
UserService.verifyEmailConfirmation = (token) => {
return UserService
.verifyToken(token, {
subject: EMAIL_CONFIRM_JWT_SUBJECT
})
.then(({userID, email}) => {
return UserModel
.update({
id: userID,
profiles: {
$elemMatch: {
id: email,
provider: 'local'
}
}
}, {
$set: {
'profiles.$.metadata.confirmed_at': new Date()
}
});
});
};
+3 -4
View File
@@ -9,8 +9,8 @@
"build-watch": "NODE_ENV=development webpack --config webpack.config.dev.js --watch",
"lint": "eslint bin/* .",
"lint-fix": "eslint bin/* . --fix",
"test": "NODE_ENV=test mocha --compilers js:babel-core/register tests/helpers/*.js --require ignore-styles --recursive tests",
"test-watch": "NODE_ENV=test mocha --compilers js:babel-core/register --recursive -w tests",
"test": "TEST_MODE=unit NODE_ENV=test mocha --compilers js:babel-core/register tests/helpers/*.js --require ignore-styles --recursive tests",
"test-watch": "TEST_MODE=unit NODE_ENV=test mocha --compilers js:babel-core/register --recursive -w tests",
"pree2e": "NODE_ENV=test scripts/pree2e.sh",
"e2e": "NODE_ENV=test nightwatch",
"embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs",
@@ -52,6 +52,7 @@
"cli-table": "^0.3.1",
"commander": "^2.9.0",
"connect-redis": "^3.1.0",
"csurf": "^1.9.0",
"debug": "^2.2.0",
"ejs": "^2.5.2",
"env-rewrite": "^1.0.2",
@@ -92,9 +93,7 @@
"babel-preset-stage-0": "^6.16.0",
"chai": "^3.5.0",
"chai-http": "^3.0.0",
"cookie-parser": "^1.4.3",
"copy-webpack-plugin": "^4.0.0",
"csurf": "^1.9.0",
"css-loader": "^0.25.0",
"dialog-polyfill": "^0.4.4",
"enzyme": "^2.6.0",
+141
View File
@@ -0,0 +1,141 @@
const express = require('express');
const router = express.Router();
const User = require('../../../models/user');
const mailer = require('../../../services/mailer');
const authorization = require('../../../middleware/authorization');
// 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;
//==============================================================================
// ROUTES
//==============================================================================
router.get('/', authorization.needed(), (req, res, next) => {
res.json(req.user);
});
// POST /email/confirm takes the password confirmation token available as a
// payload parameter and if it verifies, it updates the confirmed_at date on the
// local profile.
router.post('/email/confirm', (req, res, next) => {
const {
token
} = req.body;
if (!token) {
return next(ErrMissingToken);
}
User
.verifyEmailConfirmation(token)
.then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
});
});
/**
* 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);
});
});
/**
* 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('/settings', authorization.needed(), (req, res, next) => {
const {
bio
} = req.body;
User
.updateSettings(req.user.id, {bio})
.then(() => {
res.status(204).end();
})
.catch((err) => {
next(err);
});
});
module.exports = router;
+1 -9
View File
@@ -4,14 +4,6 @@ const router = express.Router();
const Asset = require('../../../models/asset');
const scraper = require('../../../services/scraper');
const csrf = require('csurf');
const bodyParser = require('body-parser');
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
const csrfProtection = csrf({});
const parseForm = bodyParser.urlencoded({extended: false});
// List assets.
router.get('/', (req, res, next) => {
@@ -96,7 +88,7 @@ router.get('/:asset_id', (req, res, next) => {
});
// Adds the asset id to the queue to be scraped.
router.post('/:asset_id/scrape', parseForm, csrfProtection, (req, res, next) => {
router.post('/:asset_id/scrape', (req, res, next) => {
// Create a new asset scrape job.
Asset
+12 -17
View File
@@ -4,40 +4,35 @@ const authorization = require('../../../middleware/authorization');
const router = express.Router();
const csrf = require('csurf');
const bodyParser = require('body-parser');
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
const csrfProtection = csrf({});
const parseForm = bodyParser.urlencoded({extended: false});
/**
* This returns the user if they are logged in.
*/
router.get('/', csrfProtection, (req, res, next) => {
router.get('/', (req, res, next) => {
if (req.user) {
return next();
}
// When there is no user on the request, then just send back the CSRF token.
res.json({csrfToken: req.csrfToken()});
res.status(204).end();
}, (req, res) => {
// Send back the user object.
res.json({user: req.user.toObject(), csrfToken: req.csrfToken()});
res.json({user: req.user.toObject()});
});
/**
* This destroys the session of a user, if they have one.
*/
router.delete('/', authorization.needed(), (req, res) => {
req.session.destroy(() => {
res.status(204).end();
});
delete req.session.passport;
res.status(204).end();
});
//==============================================================================
// PASSPORT ROUTES
//==============================================================================
/**
* This sends back the user data as JSON.
*/
@@ -57,7 +52,7 @@ const HandleAuthCallback = (req, res, next) => (err, user) => {
}
// We logged in the user! Let's send back the user data and the CSRF token.
res.json({user, '_csrf': req.csrfToken()});
res.json({user});
});
};
@@ -87,7 +82,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
/**
* Local auth endpoint, will recieve a email and password
*/
router.post('/local', parseForm, csrfProtection, (req, res, next) => {
router.post('/local', (req, res, next) => {
// Perform the local authentication.
passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next);
+2 -10
View File
@@ -7,14 +7,6 @@ const wordlist = require('../../../services/wordlist');
const authorization = require('../../../middleware/authorization');
const _ = require('lodash');
const csrf = require('csurf');
const bodyParser = require('body-parser');
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
const csrfProtection = csrf({});
const parseForm = bodyParser.urlencoded({extended: false});
const router = express.Router();
router.get('/', (req, res, next) => {
@@ -90,7 +82,7 @@ router.get('/', (req, res, next) => {
});
});
router.post('/', parseForm, csrfProtection, wordlist.filter('body'), (req, res, next) => {
router.post('/', wordlist.filter('body'), (req, res, next) => {
const {
body,
@@ -203,7 +195,7 @@ router.put('/:comment_id/status', authorization.needed('admin'), (req, res, next
});
});
router.post('/:comment_id/actions', parseForm, csrfProtection, (req, res, next) => {
router.post('/:comment_id/actions', (req, res, next) => {
const {
action_type,
+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);
+1
View File
@@ -8,6 +8,7 @@ const User = require('../../../models/user');
const Action = require('../../../models/action');
const Asset = require('../../../models/asset');
const Setting = require('../../../models/setting');
const ErrInvalidAssetURL = new Error('asset_url is invalid');
ErrInvalidAssetURL.status = 400;
+48 -110
View File
@@ -1,22 +1,10 @@
const express = require('express');
const router = express.Router();
const User = require('../../../models/user');
const Setting = require('../../../models/setting');
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');
const csrf = require('csurf');
const bodyParser = require('body-parser');
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
const csrfProtection = csrf({});
const parseForm = bodyParser.urlencoded({extended: false});
router.get('/', authorization.needed('admin'), (req, res, next) => {
const {
value = '',
@@ -46,7 +34,7 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
.catch(next);
});
router.post('/:user_id/role', parseForm, csrfProtection, authorization.needed('admin'), (req, res, next) => {
router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) => {
User
.addRoleToUser(req.params.user_id, req.body.role)
.then(() => {
@@ -55,119 +43,69 @@ router.post('/:user_id/role', parseForm, csrfProtection, authorization.needed('a
.catch(next);
});
router.post('/:user_id/status', parseForm, csrfProtection, (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('/', parseForm, csrfProtection, (req, res, next) => {
const {email, password, displayName} = req.body;
router.post('/', (req, res, next) => {
const {
email,
password,
displayName
} = req.body;
User
.createLocalUser(email, password, displayName)
.then(user => {
.then((user) => {
res.status(201).json(user);
// Get the settings from the database to find out if we need to send an
// email confirmation. The Front end will know about the
// requireEmailConfirmation as it's included in the settings get endpoint.
return Setting.retrieve().then(({requireEmailConfirmation = false}) => {
if (requireEmailConfirmation) {
// Email confirmation is required, let's generate that token and send
// the email.
return User
.createEmailConfirmToken(user.id, email)
.then((token) => {
return mailer.sendSimple({
app: req.app, // needed to render the templates.
template: 'email/email-confirm', // needed to know which template to render!
locals: { // specifies the template locals.
token,
rootURL: process.env.TALK_ROOT_URL,
email
},
subject: 'Email Confirmation - Talk',
to: email
});
})
.then(() => {
// Then send back the user.
res.status(201).json(user);
});
} else {
// We don't need to confirm the email, let's just send back the user!
res.status(201).json(user);
}
});
})
.catch(err => {
next(err);
});
});
const ErrPasswordTooShort = new Error('password must be at least 8 characters');
ErrPasswordTooShort.status = 400;
router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
/**
* 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', parseForm, csrfProtection, (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', parseForm, csrfProtection, (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', parseForm, csrfProtection, authorization.needed(), (req, res, next) => {
const {
action_type,
metadata
+4 -11
View File
@@ -1,28 +1,21 @@
const express = require('express');
const router = express.Router();
const csrf = require('csurf');
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
const csrfProtection = csrf({});
router.use('/api/v1', require('./api'));
router.use('/admin', require('./admin'));
router.use('/embed', require('./embed'));
router.get('/', csrfProtection, (req, res) => {
router.get('/', (req, res) => {
return res.render('article', {
title: 'Coral Talk',
basePath: '/client/embed/stream',
csrfToken: req.csrfToken()
basePath: '/client/embed/stream'
});
});
router.get('/assets/:asset_title', csrfProtection, (req, res) => {
router.get('/assets/:asset_title', (req, res) => {
return res.render('article', {
title: req.params.asset_title.split('-').join(' '),
basePath: '/client/embed/stream',
_csrf: req.csrfToken()
basePath: '/client/embed/stream'
});
});
+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
./bin/cli-users create --flag_mode --email "admin@test.com" --password "test" --name "Admin Test User" --role "admin"
# Creating Moderator Test User
{ echo moderator@test.com; echo test; echo test; echo Moderator Test User; echo moderator;} | dotenv ./bin/cli-users create
./bin/cli-users create --flag_mode --email "moderator@test.com" --password "test" --name "Moderator Test User" --role "moderator"
# Creating Commenter Test User
{ echo commenter@test.com; echo test; echo test; echo Commenter Test User; echo ;} | dotenv ./bin/cli-users create
./bin/cli-users create --flag_mode --email "commenter@test.com" --password "test" --name "commenter@test.com"
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();
});
});
}
};
-1
View File
@@ -9,7 +9,6 @@
],
"extends": "../.eslintrc.json",
"rules": {
"no-undef": [0],
"mocha/no-exclusive-tests": "warn"
}
}
@@ -104,7 +104,7 @@ describe('itemActions', () => {
});
it('should handle an error', () => {
fetchMock.get('*', 404);
return actions.getItemsArray(ids, host)(store.dispatch)
return actions.getItemsArray(ids)(store.dispatch)
.catch((err) => {
expect(err).to.be.truthy;
});
+11 -3
View File
@@ -1,4 +1,4 @@
const utils = require('../../utils/e2e-mongoose');
const mongoose = require('../../helpers/mongoose');
const mocks = require('../mocks');
const mockComment = 'This is a test comment.';
@@ -12,7 +12,11 @@ const mockUser = {
module.exports = {
'@tags': ['embed-stream', 'comment', 'premodoff', 'premodon'],
before: () => {
utils.before();
mongoose.waitTillConnect(function(err) {
if (err) {
console.error(err);
}
});
},
'User registers and posts a comment with premod off': client => {
client.perform((client, done) => {
@@ -171,7 +175,11 @@ module.exports = {
});
},
after: client => {
utils.after();
mongoose.disconnect(function(err) {
if (err) {
console.error(err);
}
});
client.end();
}
};
+3 -2
View File
@@ -1,3 +1,5 @@
const uuid = require('uuid');
module.exports = {
'@tags': ['signup', 'visitor'],
before: client => {
@@ -9,11 +11,10 @@ module.exports = {
},
'Visitor signs up': client => {
const embedStreamPage = client.page.embedStreamPage();
const hash = Math.floor(Math.random() * (999 - 0));
embedStreamPage
.signUp({
email: `visitor_${hash}@test.com`,
email: `visitor_${uuid.v4()}@test.com`,
displayName: 'Visitor',
pass: 'testtest'
});
+2
View File
@@ -1,3 +1,5 @@
/* eslint-env browser */
const jsdom = require('jsdom').jsdom;
const fs = require('fs');
const path = require('path');
+38
View File
@@ -0,0 +1,38 @@
const mongoose = require('../../services/mongoose');
module.exports = {};
module.exports.waitTillConnect = function(done) {
mongoose.connection.on('open', function(err) {
if (err) {
return done(err);
}
return done();
});
};
module.exports.clearDB = function(done) {
Promise.all(Object.keys(mongoose.connection.collections).map((collection) => {
return new Promise((resolve, reject) => {
mongoose.connection.collections[collection].remove(function(err) {
if (err) {
return reject(err);
}
return resolve();
});
});
}))
.then(() => {
done();
})
.catch((err) => {
done(err);
});
};
module.exports.disconnect = function(done) {
mongoose.disconnect();
return done();
};
+23 -18
View File
@@ -4,24 +4,29 @@ const expect = require('chai').expect;
describe('models.Action', () => {
let mockActions = [];
beforeEach(() => Action.create([{
action_type: 'flag',
item_id: '123',
item_type: 'comment',
user_id: 'flagginguserid'
}, {
action_type: 'flag',
item_id: '456',
item_type: 'comment'
}, {
action_type: 'flag',
item_id: '123',
item_type: 'comment'
}, {
action_type: 'like',
item_id: '123',
item_type: 'comment'
}]).then((actions) => {
beforeEach(() => Action.create([
{
action_type: 'flag',
item_id: '123',
item_type: 'comment',
user_id: 'flagginguserid'
},
{
action_type: 'flag',
item_id: '456',
item_type: 'comment'
},
{
action_type: 'flag',
item_id: '123',
item_type: 'comment'
},
{
action_type: 'like',
item_id: '123',
item_type: 'comment'
}
]).then((actions) => {
mockActions = actions;
}));
+68
View File
@@ -80,6 +80,74 @@ describe('models.User', () => {
});
describe('#createEmailConfirmToken', () => {
it('should create a token for a valid user', () => {
return User
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
.then((token) => {
expect(token).to.not.be.null;
});
});
it('should not create a token for a user already verified', () => {
return User
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
.then((token) => {
expect(token).to.not.be.null;
return User.verifyEmailConfirmation(token);
})
.then(() => {
return User.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id);
})
.catch((err) => {
expect(err).to.have.property('message', 'email address already confirmed');
});
});
});
describe('#verifyEmailConfirmation', () => {
it('should correctly validate a valid token', () => {
return User
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
.then((token) => {
expect(token).to.not.be.null;
return User.verifyEmailConfirmation(token);
});
});
it('should correctly reject an invalid token', () => {
return User
.verifyEmailConfirmation('cats')
.catch((err) => {
expect(err).to.not.be.null;
});
});
it('should update the user model when verification is complete', () => {
return User
.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)
.then((token) => {
expect(token).to.not.be.null;
return User.verifyEmailConfirmation(token);
})
.then(() => {
return User.findById(mockUsers[0].id);
})
.then((user) => {
expect(user.profiles[0]).to.have.property('metadata');
expect(user.profiles[0].metadata).to.have.property('confirmed_at');
expect(user.profiles[0].metadata.confirmed_at).to.not.be.null;
});
});
});
describe('#setStatus', () => {
it('should set the status to active', () => {
return User
+10 -22
View File
@@ -1,27 +1,15 @@
const mongoose = require('../services/mongoose');
const mongoose = require('./helpers/mongoose');
beforeEach(function (done) {
function clearDB() {
for (let collection in mongoose.connection.collections) {
mongoose.connection.collections[collection].remove(function() {});
}
return done();
}
before(function(done) {
this.timeout(30000);
if (mongoose.connection.readyState === 0) {
mongoose.on('open', function() {
if (err) {
throw err;
}
return clearDB();
});
} else {
return clearDB();
}
mongoose.waitTillConnect(done);
});
after(function (done) {
mongoose.disconnect();
return done();
beforeEach(function(done) {
mongoose.clearDB(done);
});
after(function(done) {
mongoose.disconnect(done);
});
+61 -38
View File
@@ -4,8 +4,6 @@ const expect = chai.expect;
chai.use(require('chai-http'));
const agent = chai.request.agent(app);
const User = require('../../../../models/user');
describe('/api/v1/auth', () => {
@@ -14,56 +12,81 @@ describe('/api/v1/auth', () => {
return chai.request(app)
.get('/api/v1/auth')
.then((res) => {
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('csrfToken');
expect(res.status).to.be.equal(204);
expect(res).to.not.have.a.body;
});
});
});
});
const Setting = require('../../../../models/setting');
describe('/api/v1/auth/local', () => {
beforeEach(() => {
return User.createLocalUser('maria@gmail.com', 'password!', 'Maria');
});
let mockUser;
beforeEach(() => User.createLocalUser('maria@gmail.com', 'password!', 'Maria').then((user) => {
mockUser = user;
}));
describe('#post', () => {
it('should send back the user on a successful login', () => {
return agent.get('/api/v1/auth')
.then((res) => {
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('csrfToken');
return agent.post('/api/v1/auth/local')
.send({email: 'maria@gmail.com', password: 'password!', _csrf: res.body.csrfToken})
.then((res2) => {
expect(res2).to.have.status(200);
expect(res2).to.be.json;
expect(res2.body).to.have.property('user');
expect(res2.body.user).to.have.property('displayName', 'Maria');
})
.catch((error) => {
expect(error).to.be.null;
});
})
.catch((error) => {
expect(error).to.be.null;
});
});
describe('email confirmation disabled', () => {
it('should not send back the user on a unsuccessful login', () => {
agent
.get('/api/v1/auth')
.then((res) => {
expect(res.status).to.be.equal(200);
expect(res.body).to.have.property('csrfToken');
return agent.post('/api/v1/auth/local')
.send({email: 'maria@gmail.com', password: 'password!3', _csrf: res.body.csrfToken})
beforeEach(() => Setting.init({requireEmailConfirmation: false}));
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!'})
.then((res2) => {
expect(res2).to.have.status(200);
expect(res2).to.be.json;
expect(res2.body).to.have.property('user');
expect(res2.body.user).to.have.property('displayName', 'Maria');
});
});
it('should not send back the user on a unsuccessful login', () => {
return chai.request(app)
.post('/api/v1/auth/local')
.send({email: 'maria@gmail.com', password: 'password!3'})
.catch((err) => {
expect(err).to.not.be.null;
expect(err.response).to.have.status(401);
expect(err.response.body).to.have.property('message', 'not authorized');
});
});
});
});
});
describe('email confirmation enabled', () => {
beforeEach(() => Setting.init({requireEmailConfirmation: true}));
describe('#post', () => {
it('should not allow a login from a user that is not confirmed', () => {
return chai.request(app)
.post('/api/v1/auth/local')
.send({email: 'maria@gmail.com', password: 'password!'})
.catch((err) => {
err.response.should.have.status(401);
return User.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id);
})
.then(User.verifyEmailConfirmation)
.then(() => {
return chai.request(app)
.post('/api/v1/auth/local')
.send({email: 'maria@gmail.com', password: 'password!'});
})
.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');
});
});
});
});
});
+58 -100
View File
@@ -4,8 +4,6 @@ const app = require('../../../../app');
const chai = require('chai');
const expect = chai.expect;
const agent = chai.request.agent(app);
// Setup chai.
chai.should();
chai.use(require('chai-http'));
@@ -185,66 +183,49 @@ describe('/api/v1/comments', () => {
]));
it('should create a comment', () => {
agent
.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset_id, 'parent_id': '', _csrf: resa.body.csrfToken})
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
});
return chai.request(app)
.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset_id, 'parent_id': ''})
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
});
});
it('should create a comment with a rejected status if it contains a bad word', () => {
agent
.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': asset_id, 'parent_id': '', _csrf: resa.body.csrfToken})
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('status', 'rejected');
});
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': asset_id, 'parent_id': ''})
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('status', 'rejected');
});
});
it('should create a comment with no status and a flag if it contains a suspected word', () => {
agent
.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'suspect words are the most suspicious', 'author_id': '123', 'asset_id': postmod_asset_id, 'parent_id': ''})
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('status', null);
return Promise.all([
res.body,
Action.findByType('flag', 'comments')
]);
})
.then(([comment, actions]) => {
expect(actions).to.have.length(1);
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'suspect words are the most suspicious', 'author_id': '123', 'asset_id': postmod_asset_id, 'parent_id': ''})
.then((res) => {
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('status', null);
return Promise.all([
res.body,
Action.findByType('flag', 'comments')
]);
})
.then(([comment, actions]) => {
expect(actions).to.have.length(1);
let action = actions[0];
let action = actions[0];
expect(action).to.have.property('item_id', comment.id);
expect(action).to.have.property('metadata');
expect(action.metadata).to.have.property('field', 'body');
expect(action.metadata).to.have.property('details', 'Matched suspect word filters.');
});
expect(action).to.have.property('item_id', comment.id);
expect(action).to.have.property('metadata');
expect(action.metadata).to.have.property('field', 'body');
expect(action.metadata).to.have.property('details', 'Matched suspect word filters.');
});
});
@@ -257,14 +238,9 @@ describe('/api/v1/comments', () => {
.then(() => asset);
})
.then((asset) => {
return agent.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': '', _csrf: resa.body.csrfToken});
});
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
})
.then((res) => {
expect(res).to.have.status(201);
@@ -283,14 +259,9 @@ describe('/api/v1/comments', () => {
.then(() => asset);
})
.then((asset) => {
return agent.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'This is way way way way way too long.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': '', _csrf: resa.body.csrfToken});
});
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'This is way way way way way too long.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
})
.then((res) => {
expect(res).to.have.status(201);
@@ -306,14 +277,9 @@ describe('/api/v1/comments', () => {
closedMessage: 'tests said expired!'
})
.then((asset) => {
return agent.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': '', _csrf: resa.body.csrfToken});
});
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
})
.then((res) => {
expect(res).to.have.status(500);
@@ -331,14 +297,9 @@ describe('/api/v1/comments', () => {
closedMessage: 'tests said expired!'
})
.then((asset) => {
return agent.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': '', _csrf: resa.body.csrfToken});
});
return chai.request(app).post('/api/v1/comments')
.set(passport.inject({roles: []}))
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
})
.then((res) => {
expect(res).to.have.status(201);
@@ -500,22 +461,19 @@ describe('/api/v1/comments/:comment_id/actions', () => {
});
describe('#post', () => {
it('it should update actions', () => {
agent.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/comments/abc/actions')
.set(passport.inject({id: '456', roles: ['admin']}))
.send({'action_type': 'flag', 'detail': 'Comment is too awesome.', _csrf: resa.csrfToken})
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('metadata')
.and.to.deep.equal({'reason': 'Comment is too awesome.'});
expect(res.body).to.have.property('item_id', 'abc');
});
it('it should create an action', () => {
return chai.request(app)
.post('/api/v1/comments/abc/actions')
.set(passport.inject({id: '456', roles: ['admin']}))
.send({'action_type': 'flag', 'metadata': {'reason': 'Comment is too awesome.'}})
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('metadata');
expect(res.body.metadata).to.deep.equal({'reason': 'Comment is too awesome.'});
expect(res.body).to.have.property('item_id', 'abc');
});
});
});
+3 -5
View File
@@ -81,17 +81,15 @@ describe('/api/v1/queue', () => {
});
});
it('should return all the pending comments, users and actions', function(done){
chai.request(app)
it('should return all the pending comments, users and actions', () => {
return chai.request(app)
.get('/api/v1/queue/comments/pending')
.set(passport.inject({roles: ['admin']}))
.end(function(err, res){
expect(err).to.be.null;
.then((res) => {
expect(res).to.have.status(200);
expect(res.body.comments[0]).to.have.property('body');
expect(res.body.users[0]).to.have.property('displayName');
expect(res.body.actions[0]).to.have.property('action_type');
done();
});
});
});
+11 -19
View File
@@ -4,8 +4,6 @@ const app = require('../../../../app');
const chai = require('chai');
const expect = chai.expect;
const agent = chai.request.agent(app);
// Setup chai.
chai.should();
chai.use(require('chai-http'));
@@ -30,23 +28,17 @@ describe('/api/v1/users/:user_id/actions', () => {
describe('#post', () => {
it('it should update actions', () => {
agent
.get('/api/v1/auth')
.then((resa) => {
expect(resa.status).to.be.equal(200);
expect(resa.body).to.have.property('csrfToken');
return agent.post('/api/v1/users/abc/actions')
.set(passport.inject({id: '456', roles: ['admin']}))
.send({'action_type': 'flag', 'detail': 'Bio is too awesome.', _csrf: resa.csrfToken})
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('metadata')
.and.to.deep.equal({'reason': 'Bio is too awesome.'});
expect(res.body).to.have.property('item_id', 'abc');
})
.catch(err => console.error(err.message));
return chai.request(app)
.post('/api/v1/users/abc/actions')
.set(passport.inject({id: '456', roles: ['admin']}))
.send({'action_type': 'flag', metadata: {reason: 'Bio is too awesome.'}})
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.have.body;
expect(res.body).to.have.property('action_type', 'flag');
expect(res.body).to.have.property('metadata')
.and.to.deep.equal({'reason': 'Bio is too awesome.'});
expect(res.body).to.have.property('item_id', 'abc');
});
});
});
-36
View File
@@ -1,36 +0,0 @@
const mongoose = require('../../services/mongoose');
// Ensure the NODE_ENV is set to 'test',
// this is helpful when you would like to change behavior when testing.
function clearDB() {
// console.log('Clearing DB', mongoose.connection);
for (let i in mongoose.connection.collections) {
// console.log('Clearing', i);
mongoose.connection.collections[i].remove(function() {});
}
}
module.exports = {
before: () => {
clearDB();
},
beforeEach: () => {
if (mongoose.connection.readyState === 0) {
mongoose.on('open', function() {
if (err) {
throw err;
}
return clearDB();
});
} else {
return clearDB();
}
},
after: () => {
clearDB();
mongoose.disconnect();
}
};
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<meta property="csrf" content="<%= csrfToken %>">
<title>Talk - Coral Admin</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">
+3
View File
@@ -0,0 +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>If you did not request this, you can safely ignore this email.</p>
+9
View File
@@ -0,0 +1,9 @@
A email confirmation has been requested for the following account:
<%= email %>
To confirm the account, please visit the following link:
http://example.com/email/confirm/endpoint#<%= token %>
If you did not request this, you can safely ignore this email.
@@ -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.
+3 -3
View File
@@ -1,13 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta property="csrf" content="<%= csrfToken %>">
<link rel="stylesheet" type="text/css" href="/client/embed/stream/default.css">
<link href="https://fonts.googleapis.com/css?family=Lato|Open+Sans" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div id="coralStream"></div>
<script src="/client/embed/stream/bundle.js"></script>
</body>
</html>
</html>
+3 -2
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<meta property="csrf" content="<%= csrfToken %>">
<title>Password Reset</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">
@@ -118,9 +119,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 %>';