mirror of
https://github.com/wassname/talk.git
synced 2026-06-28 22:04:50 +08:00
Added tests + model implementation.
This commit is contained in:
@@ -14,7 +14,7 @@ const saveBioFailure = error => ({type: actions.SAVE_BIO_FAILURE, error});
|
||||
|
||||
export const saveBio = (user_id, formData) => dispatch => {
|
||||
dispatch(saveBioRequest());
|
||||
coralApi('/account/bio', {method: 'PUT', body: formData})
|
||||
coralApi('/account/settings', {method: 'PUT', body: formData})
|
||||
.then(() => {
|
||||
dispatch(addNotification('success', lang.t('successBioUpdate')));
|
||||
dispatch(saveBioSuccess(formData));
|
||||
|
||||
+2
-1
@@ -101,7 +101,8 @@ SettingSchema.method('filterForUser', function(user = false) {
|
||||
'closeTimeout',
|
||||
'closedMessage',
|
||||
'charCountEnable',
|
||||
'charCount'
|
||||
'charCount',
|
||||
'requireEmailConfirmation'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+108
-17
@@ -6,6 +6,9 @@ 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;
|
||||
@@ -526,27 +529,45 @@ UserService.createPasswordResetToken = function (email) {
|
||||
version: user.__v
|
||||
};
|
||||
|
||||
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {algorithm: 'HS256'}, {
|
||||
expiresIn: '1d'
|
||||
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, (err, decoded) => {
|
||||
|
||||
// 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 => 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));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -587,27 +608,23 @@ 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.update({
|
||||
UserService.updateSettings = (id, settings) => UserModel.update({
|
||||
id
|
||||
}, {
|
||||
$set: {
|
||||
'settings.bio': bio
|
||||
settings
|
||||
}
|
||||
});
|
||||
|
||||
@@ -625,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()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
+38
-15
@@ -4,10 +4,46 @@ 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
|
||||
@@ -52,15 +88,6 @@ router.post('/password/reset', (req, res, next) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ErrPasswordTooShort is returned when the password length is too short.
|
||||
const ErrPasswordTooShort = new Error('password must be at least 8 characters');
|
||||
ErrPasswordTooShort.status = 400;
|
||||
|
||||
// ErrMissingToken is returned in the event that the password reset is requested
|
||||
// without a token.
|
||||
const ErrMissingToken = new Error('token is required');
|
||||
ErrMissingToken.status = 400;
|
||||
|
||||
/**
|
||||
* expects 2 fields in the body of the request
|
||||
* 1) the token that was in the url of the email link {String}
|
||||
@@ -95,18 +122,14 @@ router.put('/password/reset', (req, res, next) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.put('/bio', authorization.needed(), (req, res, next) => {
|
||||
router.put('/settings', authorization.needed(), (req, res, next) => {
|
||||
|
||||
const {
|
||||
bio
|
||||
} = req.body;
|
||||
|
||||
if (!bio) {
|
||||
return next(new Error('You must submit a new bio'));
|
||||
}
|
||||
|
||||
User
|
||||
.addBio(req.user.id, bio)
|
||||
.updateSettings(req.user.id, {bio})
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
|
||||
@@ -27,7 +27,6 @@ describe('models.Action', () => {
|
||||
item_type: 'comment'
|
||||
}
|
||||
]).then((actions) => {
|
||||
console.log('all created');
|
||||
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
|
||||
|
||||
@@ -20,40 +20,73 @@ describe('/api/v1/auth', () => {
|
||||
});
|
||||
|
||||
const Setting = require('../../../../models/setting');
|
||||
const settings = {id: '1'};
|
||||
|
||||
describe('/api/v1/auth/local', () => {
|
||||
|
||||
beforeEach(() => Promise.all([
|
||||
User.createLocalUser('maria@gmail.com', 'password!', 'Maria'),
|
||||
Setting.init(settings)
|
||||
]));
|
||||
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 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');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
describe('email confirmation disabled', () => {
|
||||
|
||||
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((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');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user