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", "regIdRequired": "RegId is required",
"pushDeviceAdded": "Push device added successfully", "pushDeviceAdded": "Push device added successfully",
"pushDeviceAlreadyAdded": "The user already has the push device", "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) { module.exports = function changeClass (user, req = {}, analytics) {
let klass = _.get(req, 'query.class'); 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.stats.class = klass;
user.flags.classSelected = true; user.flags.classSelected = true;

View File

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

View File

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

View File

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

View File

@@ -12,9 +12,36 @@ describe('shared.ops.changeClass', () => {
beforeEach(() => { beforeEach(() => {
user = generateUser(); 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', () => { 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', () => { it('changes class', () => {
user.stats.class = 'healer'; user.stats.class = 'healer';
user.items.gear.owned.armor_rogue_1 = true; // eslint-disable-line camelcase 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', () => { it('has user.preferences.disableClasses === true', () => {
user.balance = 1; user.balance = 1;
user.preferences.disableClasses = true; user.preferences.disableClasses = true;
user.preferences.autoAllocate = true; user.preferences.autoAllocate = true;
user.stats.points = 45; user.stats.points = 45;
user.stats.lvl = 3;
user.stats.str = 1; user.stats.str = 1;
user.stats.con = 2; user.stats.con = 2;
user.stats.per = 3; user.stats.per = 3;
@@ -71,7 +97,7 @@ describe('shared.ops.changeClass', () => {
expect(user.stats.con).to.equal(0); expect(user.stats.con).to.equal(0);
expect(user.stats.per).to.equal(0); expect(user.stats.per).to.equal(0);
expect(user.stats.int).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); expect(user.flags.classSelected).to.equal(false);
}); });
@@ -90,7 +116,6 @@ describe('shared.ops.changeClass', () => {
it('and at least 3 gems', () => { it('and at least 3 gems', () => {
user.balance = 1; user.balance = 1;
user.stats.points = 45; user.stats.points = 45;
user.stats.lvl = 3;
user.stats.str = 1; user.stats.str = 1;
user.stats.con = 2; user.stats.con = 2;
user.stats.per = 3; user.stats.per = 3;
@@ -112,7 +137,7 @@ describe('shared.ops.changeClass', () => {
expect(user.stats.con).to.equal(0); expect(user.stats.con).to.equal(0);
expect(user.stats.per).to.equal(0); expect(user.stats.per).to.equal(0);
expect(user.stats.int).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); expect(user.flags.classSelected).to.equal(false);
}); });
}); });

View File

@@ -4,8 +4,7 @@ import passport from 'passport';
import nconf from 'nconf'; import nconf from 'nconf';
import { import {
authWithHeaders, authWithHeaders,
authWithSession, } from '../../middlewares/api-v3/auth';
} from '../../middlewares/api-v3/auth';
import { import {
NotAuthorized, NotAuthorized,
BadRequest, 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 * @apiVersion 3.0.0
* @apiName UserRegisterLocal * @apiName UserRegisterLocal
* @apiGroup User * @apiGroup User
* *
* @apiParam {String} username Username of the new user * @apiParam {String} username Body parameter - Username of the new user
* @apiParam {String} email Email address of the new user * @apiParam {String} email Body parameter - Email address of the new user
* @apiParam {String} password Password for the new user account * @apiParam {String} password Body parameter - Password for the new user
* @apiParam {String} confirmPassword Password confirmation * @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 = { api.registerLocal = {
method: 'POST', 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 * @apiVersion 3.0.0
* @apiName UserLoginLocal * @apiName UserLoginLocal
* @apiGroup User * @apiGroup User
* *
* @apiParam {String} username Username or email of the user * @apiParam {String} username Body parameter - Username or email of the user
* @apiParam {String} password The user's password * @apiParam {String} password Body parameter - The user's password
* *
* @apiSuccess {String} _id The user's unique identifier * @apiSuccess {String} _id The user's unique identifier
* @apiSuccess {String} apiToken The user's api token that must be used to authenticate requests. * @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; 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 = { api.loginSocial = {
method: 'POST', method: 'POST',
url: '/user/auth/social', // this isn't the most appropriate url but must be the same as v2 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 * @apiVersion 3.0.0
* @apiName updateUsername * @apiName UpdateUsername
* @apiGroup User * @apiGroup User
* @apiParam {string} password The password *
* @apiParam {string} username New username * @apiParam {string} password Body parameter - The current user password
* @apiSuccess {Object} The new username * @apiParam {string} username Body parameter - The new username
* @apiSuccess {String} username The new username
**/ **/
api.updateUsername = { api.updateUsername = {
method: 'PUT', method: 'PUT',
@@ -326,13 +330,16 @@ api.updateUsername = {
/** /**
* @api {put} /api/v3/user/auth/update-password * @api {put} /api/v3/user/auth/update-password
* @apiDescription Update the password of a local user
* @apiVersion 3.0.0 * @apiVersion 3.0.0
* @apiName updatePassword * @apiName UpdatePassword
* @apiGroup User * @apiGroup User
* @apiParam {string} password The old password *
* @apiParam {string} newPassword The new password * @apiParam {string} password Body parameter - The old password
* @apiParam {string} confirmPassword Password confirmation * @apiParam {string} newPassword Body parameter - The new password
* @apiSuccess {Object} The success message * @apiParam {string} confirmPassword Body parameter - New password confirmation
*
* @apiSuccess {Object} emoty An empty object
**/ **/
api.updatePassword = { api.updatePassword = {
method: 'PUT', 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 * @apiVersion 3.0.0
* @apiName resetPassword * @apiName ResetPassword
* @apiGroup User * @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 = { api.resetPassword = {
method: 'POST', 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 * @apiVersion 3.0.0
* @apiName UpdateEmail * @apiName UpdateEmail
* @apiGroup User * @apiGroup User
* *
* @apiParam {string} newEmail The new email address. * @apiParam {string} Body parameter - newEmail The new email address.
* @apiParam {string} password The user password. * @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 = { api.updateEmail = {
method: 'PUT', method: 'PUT',
@@ -450,7 +461,7 @@ api.updateEmail = {
const firebaseTokenGenerator = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET')); const firebaseTokenGenerator = new FirebaseTokenGenerator(nconf.get('FIREBASE:SECRET'));
// Internal route TODO expose? // Internal route
api.getFirebaseToken = { api.getFirebaseToken = {
method: 'POST', method: 'POST',
url: '/user/auth/firebase', 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 * @apiVersion 3.0.0
* @apiName UserDeleteSocial * @apiName UserDeleteSocial
* @apiGroup User * @apiGroup User
* *
* @apiSuccess {Object} response Empty object * @apiSuccess {Object} empty Empty object
*/ */
api.deleteSocial = { api.deleteSocial = {
method: 'DELETE', 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; module.exports = api;

View File

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