mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 04:43:02 +08:00
Merge branch 'master' into issue-191
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
|
||||
@@ -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: ''});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -11,8 +11,7 @@ const initialState = Map({
|
||||
error: '',
|
||||
passwordRequestSuccess: null,
|
||||
passwordRequestFailure: null,
|
||||
successSignUp: false,
|
||||
_csrf: ''
|
||||
successSignUp: false
|
||||
});
|
||||
|
||||
const purge = user => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
+7
-17
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
const jsdom = require('jsdom').jsdom;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}));
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
<% } %>
|
||||
@@ -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.
|
||||
@@ -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,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 %>';
|
||||
|
||||
Reference in New Issue
Block a user