v3: several fixes to class system, move /logout outside of api

This commit is contained in:
Matteo Pagliazzi
2016-04-14 20:05:30 +02:00
parent 7562a589c5
commit 11b5c1b405
9 changed files with 109 additions and 55 deletions

View File

@@ -169,5 +169,6 @@
"regIdRequired": "RegId is required",
"pushDeviceAdded": "Push device added successfully",
"pushDeviceAlreadyAdded": "The user already has the push device",
"resetComplete": "Reset has completed"
"resetComplete": "Reset completed",
"lvl10ChangeClass": "To change class you must be at least level 10."
}

View File

@@ -9,7 +9,10 @@ import {
module.exports = function changeClass (user, req = {}, analytics) {
let klass = _.get(req, 'query.class');
if (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer') {
// user.flags.classSelected is set to false after the user paid the 3 gems
if (user.stats.lvl < 10) {
throw new NotAuthorized(i18n.t('lvl10ChangeClass', req.language));
} else if (!user.flags.classSelected && (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer')) {
user.stats.class = klass;
user.flags.classSelected = true;

View File

@@ -162,7 +162,6 @@
"name": "habitica",
"title": "Habitica",
"version": "3.0.0",
"url": "https://habitica-v3.herokuapp.com",
"sampleUrl": "https://habitica-v3.herokuapp.com"
"url": "https://habitica-v3.herokuapp.com/api-v3"
}
}

View File

@@ -6,7 +6,10 @@ describe('POST /user/change-class', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'flags.classSelected': false,
'stats.lvl': 10,
});
});
// More tests in common code unit tests

View File

@@ -57,6 +57,7 @@ describe('PUT /user', () => {
'flags unless whitelisted': {'flags.dropsEnabled': true},
webhooks: {'preferences.webhooks': [1, 2, 3]},
sleep: {'preferences.sleep': true},
'disable classes': {'preferences.disableClasses': true},
};
each(protectedOperations, (data, testName) => {

View File

@@ -12,9 +12,36 @@ describe('shared.ops.changeClass', () => {
beforeEach(() => {
user = generateUser();
user.stats.lvl = 11;
user.stats.flagSelected = false;
});
it('user is not level 10', (done) => {
user.stats.lvl = 9;
try {
changeClass(user, {query: {class: 'rogue'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('lvl10ChangeClass'));
done();
}
});
context('req.query.class is a valid class', () => {
it('errors if user.stats.flagSelected is true and user.balance < 0.75', (done) => {
user.flags.classSelected = true;
user.preferences.disableClasses = false;
user.balance = 0;
try {
changeClass(user, {query: {class: 'rogue'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notEnoughGems'));
done();
}
});
it('changes class', () => {
user.stats.class = 'healer';
user.items.gear.owned.armor_rogue_1 = true; // eslint-disable-line camelcase
@@ -41,13 +68,12 @@ describe('shared.ops.changeClass', () => {
});
});
context('req.query.class is missing', () => {
context('req.query.class is missing or user.stats.flagSelected is true', () => {
it('has user.preferences.disableClasses === true', () => {
user.balance = 1;
user.preferences.disableClasses = true;
user.preferences.autoAllocate = true;
user.stats.points = 45;
user.stats.lvl = 3;
user.stats.str = 1;
user.stats.con = 2;
user.stats.per = 3;
@@ -71,7 +97,7 @@ describe('shared.ops.changeClass', () => {
expect(user.stats.con).to.equal(0);
expect(user.stats.per).to.equal(0);
expect(user.stats.int).to.equal(0);
expect(user.stats.points).to.equal(3);
expect(user.stats.points).to.equal(11);
expect(user.flags.classSelected).to.equal(false);
});
@@ -90,7 +116,6 @@ describe('shared.ops.changeClass', () => {
it('and at least 3 gems', () => {
user.balance = 1;
user.stats.points = 45;
user.stats.lvl = 3;
user.stats.str = 1;
user.stats.con = 2;
user.stats.per = 3;
@@ -112,7 +137,7 @@ describe('shared.ops.changeClass', () => {
expect(user.stats.con).to.equal(0);
expect(user.stats.per).to.equal(0);
expect(user.stats.int).to.equal(0);
expect(user.stats.points).to.equal(3);
expect(user.stats.points).to.equal(11);
expect(user.flags.classSelected).to.equal(false);
});
});

View File

@@ -4,8 +4,7 @@ import passport from 'passport';
import nconf from 'nconf';
import {
authWithHeaders,
authWithSession,
} from '../../middlewares/api-v3/auth';
} from '../../middlewares/api-v3/auth';
import {
NotAuthorized,
BadRequest,
@@ -52,17 +51,18 @@ async function _handleGroupInvitation (user, invite) {
}
/**
* @api {post} /api/v3/user/auth/local/register Register a new user with email, username and password or attach local auth to a social user
* @api {post} /api/v3/user/auth/local/register Register
* @apiDescription Register a new user with email, username and password or attach local auth to a social user
* @apiVersion 3.0.0
* @apiName UserRegisterLocal
* @apiGroup User
*
* @apiParam {String} username Username of the new user
* @apiParam {String} email Email address of the new user
* @apiParam {String} password Password for the new user account
* @apiParam {String} confirmPassword Password confirmation
* @apiParam {String} username Body parameter - Username of the new user
* @apiParam {String} email Body parameter - Email address of the new user
* @apiParam {String} password Body parameter - Password for the new user
* @apiParam {String} confirmPassword Body parameter - Password confirmation
*
* @apiSuccess {Object} user The user object, if we just attached local auth to a social user then only user.auth.local
* @apiSuccess {Object} user The user object, if local auth was just attached to a social user then only user.auth.local
*/
api.registerLocal = {
method: 'POST',
@@ -165,13 +165,14 @@ function _loginRes (user, req, res) {
}
/**
* @api {post} /api/v3/user/auth/local/login Login an user with email / username and password
* @api {post} /api/v3/user/auth/local/login Login
* @apiDescription Login an user with email / username and password
* @apiVersion 3.0.0
* @apiName UserLoginLocal
* @apiGroup User
*
* @apiParam {String} username Username or email of the user
* @apiParam {String} password The user's password
* @apiParam {String} username Body parameter - Username or email of the user
* @apiParam {String} password Body parameter - The user's password
*
* @apiSuccess {String} _id The user's unique identifier
* @apiSuccess {String} apiToken The user's api token that must be used to authenticate requests.
@@ -227,7 +228,7 @@ function _passportFbProfile (accessToken) {
return deferred.promise;
}
// Called as a callback by Facebook (or other social providers)
// Called as a callback by Facebook (or other social providers). Internal route
api.loginSocial = {
method: 'POST',
url: '/user/auth/social', // this isn't the most appropriate url but must be the same as v2
@@ -280,13 +281,16 @@ api.loginSocial = {
};
/**
* @api {put} /api/v3/user/auth/update-username
* @api {put} /api/v3/user/auth/update-username Update username
* @apiDescription Update the username of a local user
* @apiVersion 3.0.0
* @apiName updateUsername
* @apiName UpdateUsername
* @apiGroup User
* @apiParam {string} password The password
* @apiParam {string} username New username
* @apiSuccess {Object} The new username
*
* @apiParam {string} password Body parameter - The current user password
* @apiParam {string} username Body parameter - The new username
* @apiSuccess {String} username The new username
**/
api.updateUsername = {
method: 'PUT',
@@ -326,13 +330,16 @@ api.updateUsername = {
/**
* @api {put} /api/v3/user/auth/update-password
* @apiDescription Update the password of a local user
* @apiVersion 3.0.0
* @apiName updatePassword
* @apiName UpdatePassword
* @apiGroup User
* @apiParam {string} password The old password
* @apiParam {string} newPassword The new password
* @apiParam {string} confirmPassword Password confirmation
* @apiSuccess {Object} The success message
*
* @apiParam {string} password Body parameter - The old password
* @apiParam {string} newPassword Body parameter - The new password
* @apiParam {string} confirmPassword Body parameter - New password confirmation
*
* @apiSuccess {Object} emoty An empty object
**/
api.updatePassword = {
method: 'PUT',
@@ -364,12 +371,15 @@ api.updatePassword = {
};
/**
* @api {post} /api/v3/user/reset-password
* @api {post} /api/v3/user/reset-password Reser password
* @apiDescription Reset the user password
* @apiVersion 3.0.0
* @apiName resetPassword
* @apiName ResetPassword
* @apiGroup User
* @apiParam {string} email email
* @apiSuccess {Object} The success message
*
* @apiParam {string} email Body parameter - The email address of the user
*
* @apiSuccess {string} message The localized success message
**/
api.resetPassword = {
method: 'POST',
@@ -414,15 +424,16 @@ api.resetPassword = {
};
/**
* @api {put} /api/v3/user/auth/update-email
* @api {put} /api/v3/user/auth/update-email Update email
* @apiDescription Che the user email
* @apiVersion 3.0.0
* @apiName UpdateEmail
* @apiGroup User
*
* @apiParam {string} newEmail The new email address.
* @apiParam {string} password The user password.
* @apiParam {string} Body parameter - newEmail The new email address.
* @apiParam {string} Body parameter - password The user password.
*
* @apiSuccess {Object} An object containing the new email address
* @apiSuccess {string} email The updated email address
*/
api.updateEmail = {
method: 'PUT',
@@ -450,7 +461,7 @@ api.updateEmail = {
const firebaseTokenGenerator = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET'));
// Internal route TODO expose?
// Internal route
api.getFirebaseToken = {
method: 'POST',
url: '/user/auth/firebase',
@@ -471,12 +482,13 @@ api.getFirebaseToken = {
};
/**
* @api {delete} /api/v3/user/auth/social/:network Delete a social authentication method (only facebook supported)
* @api {delete} /api/v3/user/auth/social/:network Delete social authentication method
* @apiDescription Remove a social authentication method (only facebook supported) from a user profile. The user must have local authentication enabled
* @apiVersion 3.0.0
* @apiName UserDeleteSocial
* @apiGroup User
*
* @apiSuccess {Object} response Empty object
* @apiSuccess {Object} empty Empty object
*/
api.deleteSocial = {
method: 'DELETE',
@@ -495,16 +507,4 @@ api.deleteSocial = {
},
};
// Internal route
api.logout = {
method: 'GET',
url: '/user/auth/logout', // TODO this is under /api/v3 route, should be accessible through habitica.com/logout
middlewares: [authWithSession],
async handler (req, res) {
req.logout(); // passportjs method
req.session = null;
res.redirect('/');
},
};
module.exports = api;

View File

@@ -109,6 +109,7 @@ let acceptablePUTPaths = _.reduce(require('./../../models/user').schema.paths, (
let restrictedPUTSubPaths = [
'stats.class',
'preferences.disableClasses',
'preferences.sleep',
'preferences.webhooks',
];

View File

@@ -0,0 +1,21 @@
import {
authWithSession,
} from '../../middlewares/api-v3/auth';
let api = {};
// Internal authentication routes
// Logout the user from the website.
api.logout = {
method: 'GET',
url: '/logout',
middlewares: [authWithSession],
async handler (req, res) {
req.logout(); // passportjs method
req.session = null;
res.redirect('/');
},
};
module.exports = api;