From 11b5c1b4056af619a079ee1100b97b96d7cfbac0 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 14 Apr 2016 20:05:30 +0200 Subject: [PATCH] v3: several fixes to class system, move /logout outside of api --- common/locales/en/api-v3.json | 3 +- common/script/ops/changeClass.js | 5 +- package.json | 3 +- .../user/POST-user_change-class.test.js | 5 +- test/api/v3/integration/user/PUT-user.test.js | 1 + test/common/ops/changeClass.js | 35 ++++++-- website/src/controllers/api-v3/auth.js | 90 +++++++++---------- website/src/controllers/api-v3/user.js | 1 + website/src/controllers/top-level/auth.js | 21 +++++ 9 files changed, 109 insertions(+), 55 deletions(-) create mode 100644 website/src/controllers/top-level/auth.js diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 3bd4f194cd..7af8dc9af0 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -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." } diff --git a/common/script/ops/changeClass.js b/common/script/ops/changeClass.js index 17a5d5c4d1..520a435ace 100644 --- a/common/script/ops/changeClass.js +++ b/common/script/ops/changeClass.js @@ -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; diff --git a/package.json b/package.json index 73b4c4f8ae..5407759afb 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/test/api/v3/integration/user/POST-user_change-class.test.js b/test/api/v3/integration/user/POST-user_change-class.test.js index 3a636d1c42..3ec8fe468c 100644 --- a/test/api/v3/integration/user/POST-user_change-class.test.js +++ b/test/api/v3/integration/user/POST-user_change-class.test.js @@ -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 diff --git a/test/api/v3/integration/user/PUT-user.test.js b/test/api/v3/integration/user/PUT-user.test.js index 7ea5b44cbf..eb623e0a4a 100644 --- a/test/api/v3/integration/user/PUT-user.test.js +++ b/test/api/v3/integration/user/PUT-user.test.js @@ -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) => { diff --git a/test/common/ops/changeClass.js b/test/common/ops/changeClass.js index 4cced258de..498dde18f2 100644 --- a/test/common/ops/changeClass.js +++ b/test/common/ops/changeClass.js @@ -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); }); }); diff --git a/website/src/controllers/api-v3/auth.js b/website/src/controllers/api-v3/auth.js index a98cf0310d..640003bb6d 100644 --- a/website/src/controllers/api-v3/auth.js +++ b/website/src/controllers/api-v3/auth.js @@ -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; diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index f4e63bb400..d743c9e9a8 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -109,6 +109,7 @@ let acceptablePUTPaths = _.reduce(require('./../../models/user').schema.paths, ( let restrictedPUTSubPaths = [ 'stats.class', + 'preferences.disableClasses', 'preferences.sleep', 'preferences.webhooks', ]; diff --git a/website/src/controllers/top-level/auth.js b/website/src/controllers/top-level/auth.js new file mode 100644 index 0000000000..31dfb3b693 --- /dev/null +++ b/website/src/controllers/top-level/auth.js @@ -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;