mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
v3: several fixes to class system, move /logout outside of api
This commit is contained in:
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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,
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
|
|||||||
21
website/src/controllers/top-level/auth.js
Normal file
21
website/src/controllers/top-level/auth.js
Normal 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;
|
||||||
Reference in New Issue
Block a user