mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
fix: Corrects logic where it was possible to customize avatar
with gem purchasable items without purchasing them. Closes #6427 Closes #6448 lint(common): Add unnecessary quotes to make object look less weird
This commit is contained in:
62
common/script/content/appearance.js
Normal file
62
common/script/content/appearance.js
Normal file
@@ -0,0 +1,62 @@
|
||||
export let defaultAppearancePreferences = {
|
||||
background: {},
|
||||
hair: {
|
||||
bangs: {
|
||||
0: true,
|
||||
1: true,
|
||||
2: true,
|
||||
3: true,
|
||||
},
|
||||
base: {
|
||||
0: true,
|
||||
1: true,
|
||||
3: true,
|
||||
},
|
||||
beard: {
|
||||
0: true,
|
||||
},
|
||||
color: {
|
||||
white: true,
|
||||
brown: true,
|
||||
blond: true,
|
||||
red: true,
|
||||
black: true,
|
||||
},
|
||||
flower: {
|
||||
0: true,
|
||||
1: true,
|
||||
2: true,
|
||||
3: true,
|
||||
4: true,
|
||||
5: true,
|
||||
6: true,
|
||||
},
|
||||
mustache: {
|
||||
0: true,
|
||||
},
|
||||
},
|
||||
shirt: {
|
||||
black: true,
|
||||
blue: true,
|
||||
green: true,
|
||||
pink: true,
|
||||
white: true,
|
||||
yellow: true,
|
||||
},
|
||||
size: {
|
||||
slim: true,
|
||||
broad: true,
|
||||
},
|
||||
skin: {
|
||||
/* eslint-disable quote-props */
|
||||
'ddc994': true,
|
||||
'f5a76e': true,
|
||||
'ea8349': true,
|
||||
'c06534': true,
|
||||
'98461a': true,
|
||||
'915533': true,
|
||||
'c3e1dc': true,
|
||||
'6bd049': true,
|
||||
/* eslint-enable quote-props */
|
||||
},
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import mysterySets from './mystery-sets';
|
||||
|
||||
import gear from './gear';
|
||||
import { defaultAppearancePreferences } from './appearance';
|
||||
|
||||
api.mystery = mysterySets;
|
||||
|
||||
@@ -3010,6 +3011,8 @@ api.questsByLevel = _.sortBy(api.quests, function(quest) {
|
||||
return quest.lvl || 0;
|
||||
});
|
||||
|
||||
api.defaultAppearancePreferences = defaultAppearancePreferences;
|
||||
|
||||
api.backgrounds = {
|
||||
backgrounds062014: {
|
||||
beach: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
translate as t,
|
||||
} from '../../../helpers/api-integration.helper';
|
||||
|
||||
import { each } from 'lodash';
|
||||
import { each, get } from 'lodash';
|
||||
|
||||
describe('PUT /user', () => {
|
||||
let user;
|
||||
@@ -12,7 +12,7 @@ describe('PUT /user', () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
context('allowed operations', () => {
|
||||
context('Allowed Operations', () => {
|
||||
it('updates the user', async () => {
|
||||
let updatedUser = await user.put('/user', {
|
||||
'profile.name': 'Frodo',
|
||||
@@ -26,7 +26,7 @@ describe('PUT /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('top level protected operations', () => {
|
||||
context('Top Level Protected Operations', () => {
|
||||
let protectedOperations = {
|
||||
'gem balance': {balance: 100},
|
||||
auth: {'auth.blocked': true, 'auth.timestamps.created': new Date()},
|
||||
@@ -52,7 +52,7 @@ describe('PUT /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('sub-level protected operations', () => {
|
||||
context('Sub-Level Protected Operations', () => {
|
||||
let protectedOperations = {
|
||||
'class stat': {'stats.class': 'wizard'},
|
||||
};
|
||||
@@ -71,4 +71,106 @@ describe('PUT /user', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Default Appearance Preferences', () => {
|
||||
let testCases = {
|
||||
shirt: 'yellow',
|
||||
skin: 'ddc994',
|
||||
'hair.color': 'blond',
|
||||
'hair.bangs': 2,
|
||||
'hair.base': 1,
|
||||
'hair.flower': 4,
|
||||
size: 'broad',
|
||||
};
|
||||
|
||||
each(testCases, (item, type) => {
|
||||
const update = {};
|
||||
update[`preferences.${type}`] = item;
|
||||
|
||||
it(`updates user with ${type} that is a default`, async () => {
|
||||
let dbUpdate = {};
|
||||
dbUpdate[`purchased.${type}.${item}`] = true;
|
||||
await user.update(dbUpdate);
|
||||
|
||||
// Sanity checks to make sure user is not already equipped with item
|
||||
expect(get(user.preferences, type)).to.not.eql(item);
|
||||
|
||||
let updatedUser = await user.put('/user', update);
|
||||
|
||||
expect(get(updatedUser.preferences, type)).to.eql(item);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if user tries to update body size with invalid type', async () => {
|
||||
await expect(user.put('/user', {
|
||||
'preferences.size': 'round',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
text: ['Must purchase round to set it on preferences.size'],
|
||||
});
|
||||
});
|
||||
|
||||
it('can set beard to default', async () => {
|
||||
await user.update({
|
||||
'purchased.hair.beard': 3,
|
||||
'preferences.hair.beard': 3,
|
||||
});
|
||||
|
||||
let updatedUser = await user.put('/user', {
|
||||
'preferences.hair.beard': 0,
|
||||
});
|
||||
|
||||
expect(updatedUser.preferences.hair.beard).to.eql(0);
|
||||
});
|
||||
|
||||
it('can set mustache to default', async () => {
|
||||
await user.update({
|
||||
'purchased.hair.mustache': 2,
|
||||
'preferences.hair.mustache': 2,
|
||||
});
|
||||
|
||||
let updatedUser = await user.put('/user', {
|
||||
'preferences.hair.mustache': 0,
|
||||
});
|
||||
|
||||
expect(updatedUser.preferences.hair.mustache).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
context('Purchasable Appearance Preferences', () => {
|
||||
let testCases = {
|
||||
background: 'volcano',
|
||||
shirt: 'convict',
|
||||
skin: 'cactus',
|
||||
'hair.base': 7,
|
||||
'hair.beard': 2,
|
||||
'hair.color': 'rainbow',
|
||||
'hair.mustache': 2,
|
||||
};
|
||||
|
||||
each(testCases, (item, type) => {
|
||||
const update = {};
|
||||
update[`preferences.${type}`] = item;
|
||||
|
||||
it(`returns an error if user tries to update ${type} with ${type} the user does not own`, async () => {
|
||||
await expect(user.put('/user', update)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
text: [`Must purchase ${item} to set it on preferences.${type}`],
|
||||
});
|
||||
});
|
||||
|
||||
it(`updates user with ${type} user does own`, async () => {
|
||||
let dbUpdate = {};
|
||||
dbUpdate[`purchased.${type}.${item}`] = true;
|
||||
await user.update(dbUpdate);
|
||||
|
||||
// Sanity check to make sure user is not already equipped with item
|
||||
expect(get(user.preferences, type)).to.not.eql(item);
|
||||
|
||||
let updatedUser = await user.put('/user', update);
|
||||
|
||||
expect(get(updatedUser.preferences, type)).to.eql(item);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@ var Group = require('./../../models/group').model;
|
||||
var Challenge = require('./../../models/challenge').model;
|
||||
var moment = require('moment');
|
||||
var logging = require('./../../libs/logging');
|
||||
var acceptablePUTPaths;
|
||||
let acceptablePUTPaths;
|
||||
let restrictedPUTSubPaths;
|
||||
|
||||
var api = module.exports;
|
||||
var qs = require('qs');
|
||||
var firebase = require('../../libs/firebase');
|
||||
@@ -284,17 +286,44 @@ api.getUserAnonymized = function(req, res, next) {
|
||||
* The trick here is to only accept leaf paths, not root/intermediate paths (see http://goo.gl/OEzkAs)
|
||||
* FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations
|
||||
*/
|
||||
acceptablePUTPaths = _.reduce(require('./../../models/user').schema.paths, function(m,v,leaf){
|
||||
var found= _.find('achievements filters flags invitations lastCron party preferences profile stats inbox'.split(' '), function(root){
|
||||
return leaf.indexOf(root) == 0;
|
||||
acceptablePUTPaths = _.reduce(require('./../../models/user').schema.paths, (m, v, leaf) => {
|
||||
let updatablePaths = 'achievements filters flags invitations lastCron party preferences profile stats inbox'.split(' ');
|
||||
let found = _.find(updatablePaths, (rootPath) => {
|
||||
return leaf.indexOf(rootPath) === 0;
|
||||
});
|
||||
if (found) m[leaf]=true;
|
||||
return m;
|
||||
}, {})
|
||||
|
||||
_.each('stats.class'.split(' '), function(removePath){
|
||||
if (found) m[leaf] = true;
|
||||
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
restrictedPUTSubPaths = 'stats.class'.split(' ');
|
||||
|
||||
_.each(restrictedPUTSubPaths, (removePath) => {
|
||||
delete acceptablePUTPaths[removePath];
|
||||
})
|
||||
});
|
||||
|
||||
let requiresPurchase = {
|
||||
'preferences.background': 'background',
|
||||
'preferences.shirt': 'shirt',
|
||||
'preferences.size': 'size',
|
||||
'preferences.skin': 'skin',
|
||||
'preferences.hair.bangs': 'hair.bangs',
|
||||
'preferences.hair.base': 'hair.base',
|
||||
'preferences.hair.beard': 'hair.beard',
|
||||
'preferences.hair.color': 'hair.color',
|
||||
'preferences.hair.flower': 'hair.flower',
|
||||
'preferences.hair.mustache': 'hair.mustache',
|
||||
};
|
||||
|
||||
let checkPreferencePurchase = (user, path, item) => {
|
||||
let itemPath = `${path}.${item}`;
|
||||
let isDefaultPreference = _.get(shared.content.defaultAppearancePreferences, itemPath);
|
||||
|
||||
if (isDefaultPreference) return true;
|
||||
|
||||
return _.get(user.purchased, itemPath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update user
|
||||
@@ -302,21 +331,31 @@ _.each('stats.class'.split(' '), function(removePath){
|
||||
* PUT /user {'stats.hp':50, 'tasks.TASK_ID.repeat.m':false}
|
||||
* See acceptablePUTPaths for which user paths are supported
|
||||
*/
|
||||
api.update = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
var errors = [];
|
||||
api.update = (req, res, next) => {
|
||||
let user = res.locals.user;
|
||||
let errors = [];
|
||||
|
||||
if (_.isEmpty(req.body)) return res.json(200, user);
|
||||
|
||||
_.each(req.body, function(v, k) {
|
||||
if (acceptablePUTPaths[k])
|
||||
_.each(req.body, (v, k) => {
|
||||
let purchasable = requiresPurchase[k];
|
||||
|
||||
if (purchasable && !checkPreferencePurchase(user, purchasable, v)) {
|
||||
return errors.push(`Must purchase ${v} to set it on ${k}`);
|
||||
}
|
||||
|
||||
if (acceptablePUTPaths[k]) {
|
||||
user.fns.dotSet(k, v);
|
||||
else
|
||||
} else {
|
||||
errors.push(shared.i18n.t('messageUserOperationProtected', { operation: k }));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
user.save(function(err) {
|
||||
|
||||
user.save((err) => {
|
||||
if (!_.isEmpty(errors)) return res.json(401, {err: errors});
|
||||
if (err) return next(err);
|
||||
|
||||
res.json(200, user);
|
||||
user = errors = null;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user