Compare commits

...

37 Commits

Author SHA1 Message Date
Matteo Pagliazzi
3f1343fdfb fix trailing space 2016-05-25 14:58:38 +02:00
Keith Holliday
7297a6fa76 Updated logic to not assign armoire incorrectly (#7476)
* Updated logic to not assign armoire incorrectly

* Updated test and added new test to ensure armorie is not enabled prematurely.

* Fixed linting issue

* Removed toObject from being called when called on the server
2016-05-25 14:52:17 +02:00
Matteo Pagliazzi
f713bf53c1 fix task.challenge tests and add checks when scoring challenge tasks 2016-05-25 14:49:08 +02:00
Matteo Pagliazzi
506705d738 mark includeAllMembers query parameter as beta 2016-05-25 14:47:51 +02:00
Matteo Pagliazzi
cb1d385755 fix tasks minimization 2016-05-25 14:10:31 +02:00
Matteo Pagliazzi
d8051f3bf1 fix client side tests 2016-05-25 13:08:47 +02:00
Matteo Pagliazzi
042ac16c48 client: fetch all challenges members 2016-05-25 12:41:24 +02:00
Matteo Pagliazzi
adce786ae2 feat(members): add ability to return all members 2016-05-25 12:36:52 +02:00
Matteo Pagliazzi
a771c43989 feat(test): allow logs to show up when testing 2016-05-25 12:35:58 +02:00
Blade Barringer
0008839996 Merge pull request #7475 from crookedneighbor/armoire_sync_fix
Fix Armoire returns wrong equipment bug
2016-05-24 21:02:02 -05:00
Matteo Pagliazzi
1e270e417c disable push notifications and fix buying gems 2016-05-24 23:16:28 +02:00
Keith Holliday
048d55b962 Removed sync call on load (#7485) 2016-05-24 22:41:38 +02:00
Matteo Pagliazzi
91e16ff191 do not run cron when loading party to avoid race conditions 2016-05-24 22:40:54 +02:00
Matteo Pagliazzi
c7d3539815 do not colorize or pretty print logs in production 2016-05-24 21:42:31 +02:00
Matteo Pagliazzi
2ca52785e1 remove unused variable in payments code 2016-05-24 21:41:14 +02:00
Matteo Pagliazzi
67e68ece29 fix gifting gems 2016-05-24 21:31:29 +02:00
Matteo Pagliazzi
9855f8c385 fix typo in stripe cancel 2016-05-24 18:42:08 +02:00
Blade Barringer
35544f14c5 fix: Armoire sends back data from server rather than relying on client
closes #5376
2016-05-24 07:13:09 -05:00
Blade Barringer
6c017b31fb Merge pull request #7467 from crookedneighbor/fix_change_password
fix: Correct change password on client
2016-05-24 06:45:44 -05:00
Blade Barringer
ac77ceb75f fix: Correct change password on client
* Add additional checks on server to prevent 500
* Add tests for param checks
2016-05-24 06:21:38 -05:00
Matteo Pagliazzi
6ce2f53503 Merge branch 'develop' of github.com:HabitRPG/habitrpg into develop 2016-05-24 13:11:33 +02:00
Matteo Pagliazzi
70d7a922f6 fix expression in angular template 2016-05-24 13:10:59 +02:00
Matteo Pagliazzi
64b3896797 fix typo in migration script 2016-05-24 13:10:42 +02:00
Blade Barringer
02d075e342 lint: Ignore === for == null check in sort task 2016-05-23 22:41:37 -05:00
Blade Barringer
8faf73084a fix: correct sort task when task is at the top
closes #7457

!0 === true, so we need to check for == null which matches undefined and
null
2016-05-23 22:33:18 -05:00
Alys
810355d2a5 implement cron semi-safe mode - cron behaves as normal except boss causes no damage (#7462) 2016-05-24 13:31:39 +10:00
Blade Barringer
aaf2ff5b70 fix: Correct api documentation for score route
Fixes #7458
2016-05-23 22:13:59 -05:00
Blade Barringer
977d2ae525 Merge pull request #7466 from crookedneighbor/beastmastercount_fix
fix: Display beastmaster count even if member route returned no items
2016-05-23 22:07:22 -05:00
Blade Barringer
f9b759ae57 fix: Impliment redirect for /static/api -> /apidoc 2016-05-23 22:06:06 -05:00
Blade Barringer
1f3bd45471 Merge pull request #7463 from crookedneighbor/make_admin
feat: Add make-admin debug route back in
2016-05-23 21:50:39 -05:00
Blade Barringer
b1519eed14 fix: Display beastmaster count even if member route returned no items
Because of this line:
f1286762a8/website/server/controllers/api-v3/members.js (L53)

We minimize any empty objects, which causes the statCalc methods to fail
2016-05-23 21:47:42 -05:00
Blade Barringer
f1286762a8 Merge branch 'TheHollidayInn-tasks-delete-checklist' into develop 2016-05-23 21:19:01 -05:00
Blade Barringer
770ffe93fc fix: Prevent task cloning from cloneing challenge object
fixes #7435
closes #7451
2016-05-23 21:13:35 -05:00
Blade Barringer
2ab76db27c feat: Add make-admin debug route back in 2016-05-23 20:59:47 -05:00
Blade Barringer
a099c1b3b5 Merge branch 'TheHollidayInn-hall-add-service' into develop 2016-05-23 20:59:24 -05:00
Keith Holliday
d4287e1fd8 Added sync with user adds checklist item. Prevented task checklist remove request from being called on blank checklist 2016-05-23 23:34:45 +01:00
Keith Holliday
80323120b6 Updated hall service and updated hall controller 2016-05-23 21:32:43 +01:00
40 changed files with 357 additions and 171 deletions

View File

@@ -8,7 +8,7 @@ import content from './content/index';
const DROP_ANIMALS = keys(content.pets);
function beastMasterProgress (pets) {
function beastMasterProgress (pets = {}) {
let count = 0;
each(DROP_ANIMALS, (animal) => {
@@ -19,7 +19,7 @@ function beastMasterProgress (pets) {
return count;
}
function dropPetsCurrentlyOwned (pets) {
function dropPetsCurrentlyOwned (pets = {}) {
let count = 0;
each(DROP_ANIMALS, (animal) => {
@@ -30,7 +30,7 @@ function dropPetsCurrentlyOwned (pets) {
return count;
}
function mountMasterProgress (mounts) {
function mountMasterProgress (mounts = {}) {
let count = 0;
each(DROP_ANIMALS, (animal) => {
@@ -41,7 +41,7 @@ function mountMasterProgress (mounts) {
return count;
}
function remainingGearInSet (userGear, set) {
function remainingGearInSet (userGear = {}, set) {
let gear = filter(content.gear.flat, (item) => {
let setMatches = item.klass === set;
let hasItem = userGear[item.key];
@@ -54,7 +54,7 @@ function remainingGearInSet (userGear, set) {
return count;
}
function questsOfCategory (userQuests, category) {
function questsOfCategory (userQuests = {}, category) {
let quests = filter(content.quests, (quest) => {
let categoryMatches = quest.category === category;
let hasQuest = userQuests[quest.key];

View File

@@ -15,7 +15,16 @@ module.exports = function ultimateGear (user) {
}
});
if (_.contains(user.achievements.ultimateGearSets, true) && user.flags.armoireEnabled !== true) {
let ultimateGearSetValues;
if (user.achievements.ultimateGearSets.toObject) {
ultimateGearSetValues = Object.values(user.achievements.ultimateGearSets.toObject());
} else {
ultimateGearSetValues = Object.values(user.achievements.ultimateGearSets);
}
let hasFullSet = _.includes(ultimateGearSetValues, true);
if (hasFullSet && user.flags.armoireEnabled !== true) {
user.flags.armoireEnabled = true;
}

View File

@@ -21,7 +21,7 @@ module.exports = function sortTask (user, req = {}) {
if (index === -1) {
throw new NotFound(i18n.t('messageTaskNotFound', req.language));
}
if (!to && !fromParam) {
if (to == null && fromParam == null) { // eslint-disable-line eqeqeq
throw new BadRequest('?to=__&from=__ are required');
}

View File

@@ -9,7 +9,9 @@
"NODE_DB_URI":"mongodb://localhost/habitrpg",
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
"NODE_ENV":"development",
"ENABLE_CONSOLE_LOGS_IN_TEST": false,
"CRON_SAFE_MODE":"false",
"CRON_SEMI_SAFE_MODE":"false",
"MAINTENANCE_MODE": "false",
"SESSION_SECRET":"YOUR SECRET HERE",
"ADMIN_EMAIL": "you@example.com",

View File

@@ -109,7 +109,7 @@ function processGroups (afterId) {
oldGroup.memberCount = oldGroup.members ? oldGroup.members.length : 0;
oldGroup.challengeCount = oldGroup.challenges ? oldGroup.challenges.length : 0;
if (!oldGroup.balance <= 0) oldGroup.balance = 0;
if (oldGroup.balance <= 0) oldGroup.balance = 0;
if (!oldGroup.name) oldGroup.name = 'group name';
if (!oldGroup.leaderOnly) oldGroup.leaderOnly = {};
if (!oldGroup.leaderOnly.challenges) oldGroup.leaderOnly.challenges = false;

View File

@@ -69,7 +69,25 @@ describe('GET /challenges/:challengeId/members', () => {
expect(res[0].profile).to.have.all.keys(['name']);
});
it('returns only first 30 members', async () => {
it('returns only first 30 members if req.query.includeAllMembers is not true', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
}
await Promise.all(usersToGenerate);
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=not-true`);
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});
it('returns only first 30 members if req.query.includeAllMembers is not defined', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
@@ -87,6 +105,24 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('returns all members if req.query.includeAllMembers is true', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
}
await Promise.all(usersToGenerate);
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=true`);
expect(res.length).to.equal(32);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});
it('supports using req.query.lastId to get more members', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);

View File

@@ -117,7 +117,7 @@ describe('POST /challenges/:challengeId/leave', () => {
});
expect(testTask).to.not.be.undefined;
expect(testTask.challenge).to.be.undefined;
expect(testTask.challenge).to.eql({});
});
});
});

View File

@@ -3,7 +3,7 @@ import {
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
xdescribe('POST /debug/make-admin (pended for v3 prod testing)', () => {
describe('POST /debug/make-admin (pended for v3 prod testing)', () => {
let user;
before(async () => {

View File

@@ -74,6 +74,23 @@ describe('GET /groups/:groupId/members', () => {
});
});
it('returns only first 30 members even when ?includeAllMembers=true', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
usersToGenerate.push(generateUser({party: {_id: group._id}}));
}
await Promise.all(usersToGenerate);
let res = await user.get('/groups/party/members?includeAllMembers=true');
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});
it('supports using req.query.lastId to get more members', async () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});

View File

@@ -45,7 +45,7 @@ describe('PUT /tasks/:id', () => {
expect(savedTask.history).to.eql(task.history);
expect(savedTask.createdAt).to.equal(task.createdAt);
expect(savedTask.updatedAt).to.be.greaterThan(task.updatedAt);
expect(savedTask.challenge).to.equal(task.challenge);
expect(savedTask.challenge).to.eql(task.challenge);
expect(savedTask.completed).to.eql(task.completed);
expect(savedTask.streak).to.equal(savedTask.streak); // it's an habit, dailies can change it
expect(savedTask.dateCompleted).to.equal(task.dateCompleted);

View File

@@ -50,4 +50,43 @@ describe('PUT /user/auth/update-password', async () => {
message: t('wrongPassword'),
});
});
it('returns an error when password is missing', async () => {
let body = {
newPassword,
confirmPassword: newPassword,
};
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error when newPassword is missing', async () => {
let body = {
password,
confirmPassword: newPassword,
};
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error when confirmPassword is missing', async () => {
let body = {
password,
newPassword,
};
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});

View File

@@ -8,6 +8,9 @@ describe('shared.fns.ultimateGear', () => {
beforeEach(() => {
user = generateUser();
user.achievements.ultimateGearSets.toObject = function () {
return this;
};
});
it('sets armoirEnabled when partial achievement already achieved', () => {
@@ -30,4 +33,25 @@ describe('shared.fns.ultimateGear', () => {
ultimateGear(user);
expect(user.flags.armoireEnabled).to.equal(true);
});
it('does not set armoirEnabled when gear is not owned', () => {
let items = {
gear: {
owned: {
toObject: () => {
return {
armor_warrior_5: true, // eslint-disable-line camelcase
shield_warrior_5: true, // eslint-disable-line camelcase
head_warrior_5: true, // eslint-disable-line camelcase
weapon_warrior_6: false, // eslint-disable-line camelcase
};
},
},
},
};
user.items = items;
ultimateGear(user);
expect(user.flags.armoireEnabled).to.equal(false);
});
});

View File

@@ -47,7 +47,7 @@ describe('memberServices', function() {
it('calls get challenge members', function() {
var challengeId = 1;
var memberUrl = apiV3Prefix + '/challenges/' + challengeId + '/members';
var memberUrl = apiV3Prefix + '/challenges/' + challengeId + '/members?includeAllMembers=true';
$httpBackend.expectGET(memberUrl).respond({});
members.getChallengeMembers(challengeId);
$httpBackend.flush();

View File

@@ -1,47 +1,53 @@
"use strict";
habitrpg.controller("HallHeroesCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', '$resource',
function($scope, $rootScope, User, Notification, ApiUrl, $resource) {
var Hero = $resource(ApiUrl.get() + '/api/v3/hall/heroes/:uid', {uid:'@_id'});
habitrpg.controller("HallHeroesCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', 'Hall',
function($scope, $rootScope, User, Notification, ApiUrl, Hall) {
$scope.hero = undefined;
$scope.loadHero = function(uuid){
Hero.query({uid:uuid}, function (heroData) {
$scope.hero = heroData.data;
$scope.currentHeroIndex = undefined;
$scope.heroes = [];
Hall.getHeroes()
.then(function (response) {
$scope.heroes = response.data.data;
});
$scope.loadHero = function(uuid, heroIndex) {
$scope.currentHeroIndex = heroIndex;
Hall.getHero(uuid)
.then(function (response) {
$scope.hero = response.data.data;
});
}
$scope.saveHero = function(hero) {
$scope.hero.contributor.admin = ($scope.hero.contributor.level > 7) ? true : false;
hero.$save(function(){
Notification.text("User updated");
$scope.hero = undefined;
$scope._heroID = undefined;
Hero.query({}, function (heroesData) {
$scope.heroes = heroesData.data;
Hall.updateHero($scope.hero)
.then(function (response) {
Notification.text("User updated");
$scope.hero = undefined;
$scope._heroID = undefined;
$scope.heroes[$scope.currentHeroIndex] = response.data.data;
$scope.currentHeroIndex = undefined;
});
})
}
Hero.query({}, function (heroesData) {
$scope.heroes = heroesData.data;
});
$scope.populateContributorInput = function(id) {
$scope.populateContributorInput = function(id, index) {
$scope._heroID = id;
window.scrollTo(0,200);
$scope.loadHero(id);
window.scrollTo(0, 200);
$scope.loadHero(id, index);
};
}]);
habitrpg.controller("HallPatronsCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', '$resource',
function($scope, $rootScope, User, Notification, ApiUrl, $resource) {
var Patron = $resource(ApiUrl.get() + '/api/v3/hall/patrons/:uid', {uid:'@_id'});
habitrpg.controller("HallPatronsCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', 'Hall',
function($scope, $rootScope, User, Notification, ApiUrl, Hall) {
var page = 0;
$scope.patrons = [];
$scope.loadMore = function(){
Patron.query({page: page++}, function(patronsData){
$scope.patrons = $scope.patrons.concat(patronsData.data);
})
$scope.loadMore = function() {
Hall.getPatrons(page++)
.then(function (response) {
$scope.patrons = $scope.patrons.concat(response.data.data);
});
}
$scope.loadMore();

View File

@@ -193,24 +193,27 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
// Don't allow creation of an empty checklist item
// TODO Provide UI feedback that this item is still blank
} else if ($index == task.checklist.length - 1) {
Tasks.addChecklistItem(task._id, task.checklist[$index]);
task.checklist.push({completed:false,text:''});
focusChecklist(task,task.checklist.length-1);
Tasks.addChecklistItem(task._id, task.checklist[$index])
.then(function (response) {
task.checklist[$index] = response.data.data.checklist[$index];
});
task.checklist.push({completed:false, text:''});
focusChecklist(task, task.checklist.length - 1);
} else {
$scope.saveTask(task, true);
focusChecklist(task, $index + 1);
}
}
$scope.removeChecklistItem = function(task, $event, $index, force){
$scope.removeChecklistItem = function(task, $event, $index, force) {
// Remove item if clicked on trash icon
if (force) {
Tasks.removeChecklistItem(task._id, task.checklist[$index].id);
if (task.checklist[$index].id) Tasks.removeChecklistItem(task._id, task.checklist[$index].id);
task.checklist.splice($index, 1);
} else if (!task.checklist[$index].text) {
// User deleted all the text and is now wishing to delete the item
// saveTask will prune the empty item
Tasks.removeChecklistItem(task._id, task.checklist[$index].id);
if (task.checklist[$index].id) Tasks.removeChecklistItem(task._id, task.checklist[$index].id);
// Move focus if the list is still non-empty
if ($index > 0)
focusChecklist(task, $index-1);
@@ -256,6 +259,11 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
User.buy({params:{key:item.key}});
};
$scope.buyArmoire = function () {
playRewardSound($scope.armoire);
User.buyArmoire();
}
/*
------------------------
Hiding Tasks

View File

@@ -0,0 +1,41 @@
'use strict';
angular.module('habitrpg')
.factory('Hall', [ '$rootScope', 'ApiUrl', '$http',
function($rootScope, ApiUrl, $http) {
var apiV3Prefix = '/api/v3';
var Hall = {};
Hall.getHeroes = function () {
return $http({
method: 'GET',
url: apiV3Prefix + '/hall/heroes',
});
}
Hall.getHero = function (uuid) {
return $http({
method: 'GET',
url: apiV3Prefix + '/hall/heroes/' + uuid,
});
}
Hall.updateHero = function (heroDetails) {
return $http({
method: 'PUT',
url: apiV3Prefix + '/hall/heroes/' + heroDetails._id,
data: heroDetails,
});
}
Hall.getPatrons = function (page) {
if (!page) page = 0;
return $http({
method: 'GET',
url: apiV3Prefix + '/hall/patrons?page=' + page,
});
}
return Hall;
}]);

View File

@@ -38,7 +38,7 @@ angular.module('habitrpg')
function getChallengeMembers (challengeId) {
return $http({
method: 'GET',
url: apiV3Prefix + '/challenges/' + challengeId + '/members',
url: apiV3Prefix + '/challenges/' + challengeId + '/members?includeAllMembers=true',
});
}

View File

@@ -1,6 +1,6 @@
'use strict';
var TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt'];
var TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge'];
angular.module('habitrpg')
.factory('Tasks', ['$rootScope', 'Shared', '$http',

View File

@@ -112,7 +112,6 @@ angular.module('habitrpg')
$rootScope.$emit('userSynced');
});
}
sync();
var save = function () {
localStorage.setItem(STORAGE_USER_ID, JSON.stringify(user));
@@ -355,6 +354,17 @@ angular.module('habitrpg')
callOpsFunctionAndRequest('buy', 'buy', "POST", data.params.key, data);
},
buyArmoire: function () {
$http({
method: "POST",
url: '/api/v3/user/buy-armoire',
})
.then(function (response) {
Notification.text(response.data.message);
sync();
})
},
buyQuest: function (data) {
callOpsFunctionAndRequest('buyQuest', 'buy-quest', "POST", data.params.key, data);
},

View File

@@ -56,6 +56,7 @@
"js/services/socialServices.js",
"js/services/statServices.js",
"js/services/userServices.js",
"js/services/hallServices.js",
"js/filters/money.js",
"js/filters/roundLargeNumbers.js",

View File

@@ -352,18 +352,27 @@ api.updatePassword = {
if (!user.auth.local.hashed_password) throw new BadRequest(res.t('userHasNoLocalRegistration'));
let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt);
if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword'));
req.checkBody({
password: {
notEmpty: {errorMessage: res.t('missingNewPassword')},
},
newPassword: {
notEmpty: {errorMessage: res.t('missingPassword')},
},
newPassword: {
notEmpty: {errorMessage: res.t('missingNewPassword')},
},
confirmPassword: {
notEmpty: {errorMessage: res.t('missingNewPassword')},
},
});
let validationErrors = req.validationErrors();
if (validationErrors) {
throw validationErrors;
}
let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt);
if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword'));
if (req.body.newPassword !== req.body.confirmPassword) throw new NotAuthorized(res.t('passwordConfirmationMatch'));
user.auth.local.hashed_password = passwordUtils.encrypt(req.body.newPassword, user.auth.local.salt); // eslint-disable-line camelcase

View File

@@ -88,21 +88,20 @@ api.setCron = {
*
* @apiSuccess {Object} data An empty Object
*/
// TODO: Re-enable after v3 prod testing is done
// api.makeAdmin = {
// method: 'POST',
// url: '/debug/make-admin',
// middlewares: [ensureDevelpmentMode, authWithHeaders()],
// async handler (req, res) {
// let user = res.locals.user;
//
// user.contributor.admin = true;
//
// await user.save();
//
// res.respond(200, {});
// },
// };
api.makeAdmin = {
method: 'POST',
url: '/debug/make-admin',
middlewares: [ensureDevelpmentMode, authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
user.contributor.admin = true;
await user.save();
res.respond(200, {});
},
};
/**
* @api {post} /api/v3/debug/modify-inventory Manipulate user's inventory

View File

@@ -117,6 +117,9 @@ api.getGroups = {
api.getGroup = {
method: 'GET',
url: '/groups/:groupId',
// Disable cron when getting groups to avoid race conditions when the site is loaded
// and requests for party and user data are concurrent
runCron: false,
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;

View File

@@ -85,7 +85,13 @@ function _getMembersForItem (type) {
// optionalMembership is set to true because even if you're not member of the group you may be able to access the challenge
// for example if you've been booted from it, are the leader or a site admin
group = await Group.getGroup({user, groupId: challenge.group, fields: '_id type privacy', optionalMembership: true});
group = await Group.getGroup({
user,
groupId: challenge.group,
fields: '_id type privacy',
optionalMembership: true,
});
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
} else {
group = await Group.getGroup({user, groupId, fields: '_id type'});
@@ -117,10 +123,17 @@ function _getMembersForItem (type) {
if (lastId) query._id = {$gt: lastId};
let limit = 30;
// Allow for all challenges members to be returned
if (type === 'challenge-members' && req.query.includeAllMembers === 'true') {
limit = 0; // no limit
}
let members = await User
.find(query)
.sort({_id: 1})
.limit(30)
.limit(limit)
.select(fields)
.exec();
@@ -170,14 +183,19 @@ api.getInvitesForGroup = {
/**
* @api {get} /api/v3/challenges/:challengeId/members Get members for a challenge
* @apiDescription With a limit of 30 member per request. To get all members run requests against this routes (updating the lastId query parameter) until you get less than 30 results.
* @apiDescription With a limit of 30 member per request.
* To get all members run requests against this routes (updating the lastId query parameter) until you get less than 30 results.
* BETA You can also use ?includeAllMembers=true. This option is currently in BETA and may be removed in future.
* Its use is discouraged and its performaces are not optimized especially for large challenges.
*
* @apiVersion 3.0.0
* @apiName GetMembersForChallenge
* @apiGroup Member
*
* @apiParam {UUID} challengeId The challenge id
* @apiParam {UUID} lastId Query parameter to specify the last member returned in a previous request to this route and get the next batch of results
*
* @apiParam {string} includeAllMembers BETA Query parameter - If 'true' all challenge members are returned
* @apiSuccess {array} data An array of members, sorted by _id
*/
api.getMembersForChallenge = {

View File

@@ -357,7 +357,7 @@ function _generateWebhookTaskData (task, direction, delta, stats, user) {
}
/**
* @api {put} /api/v3/tasks/:taskId/score/:direction Score a task
* @api {post} /api/v3/tasks/:taskId/score/:direction Score a task
* @apiVersion 3.0.0
* @apiName ScoreTask
* @apiGroup Task
@@ -422,13 +422,15 @@ api.scoreTask = {
sendTaskWebhook(user.preferences.webhooks, _generateWebhookTaskData(task, direction, delta, userStats, user));
if (task.challenge.id && task.challenge.taskId && !task.challenge.broken && task.type !== 'reward') {
if (task.challenge && task.challenge.id && task.challenge.taskId && !task.challenge.broken && task.type !== 'reward') {
// Wrapping everything in a try/catch block because if an error occurs using `await` it MUST NOT bubble up because the request has already been handled
try {
let chalTask = await Tasks.Task.findOne({
_id: task.challenge.taskId,
}).exec();
if (!chalTask) return;
await chalTask.scoreChallengeTask(delta);
} catch (e) {
logger.error(e);

View File

@@ -48,6 +48,15 @@ _.each(staticPages, (name) => {
};
});
api.redirectApi = {
method: 'GET',
url: '/static/api',
runCron: false,
async handler (req, res) {
res.redirect(301, '/apidoc');
},
};
let shareables = ['level-up', 'hatch-pet', 'raise-pet', 'unlock-quest', 'won-challenge', 'achievement'];
_.each(shareables, (name) => {

View File

@@ -154,7 +154,7 @@ api.subscribeCancel = {
let user = res.locals.user;
if (!user.purchased.plan.customerId) throw new NotAuthorized(res.t('missingSubscription'));
let customer = await stripe.customers.retrieve(user.purchased.plan.customeerId);
let customer = await stripe.customers.retrieve(user.purchased.plan.customerId);
await stripe.customers.del(user.purchased.plan.customerId);
await payments.cancelSubscription({
user,

View File

@@ -5,6 +5,7 @@ import _ from 'lodash';
import nconf from 'nconf';
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
const shouldDo = common.shouldDo;
const scoreTask = common.ops.scoreTask;
// const maxPMs = 200;
@@ -175,13 +176,15 @@ export function cron (options = {}) {
cron: true,
});
// Apply damage from a boss, less damage for Trivial priority (difficulty)
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident
// initially, and when we realised, we could not fix it because users are used to
// their Medium and Hard Dailies doing an Easy amount of damage from boss.
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
// setting between Trivial and Easy.
if (!CRON_SEMI_SAFE_MODE) {
// Apply damage from a boss, less damage for Trivial priority (difficulty)
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident
// initially, and when we realised, we could not fix it because users are used to
// their Medium and Hard Dailies doing an Easy amount of damage from boss.
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
// setting between Trivial and Easy.
}
}
}
}

View File

@@ -5,20 +5,19 @@ import _ from 'lodash';
const IS_PROD = nconf.get('IS_PROD');
const IS_TEST = nconf.get('IS_TEST');
const ENABLE_CONSOLE_LOGS_IN_PROD = nconf.get('ENABLE_CONSOLE_LOGS_IN_PROD') === 'true';
const ENABLE_LOGS_IN_TEST = nconf.get('ENABLE_CONSOLE_LOGS_IN_TEST') === 'true';
const ENABLE_LOGS_IN_PROD = nconf.get('ENABLE_CONSOLE_LOGS_IN_PROD') === 'true';
const logger = new winston.Logger();
if (IS_PROD) {
if (ENABLE_CONSOLE_LOGS_IN_PROD) {
if (ENABLE_LOGS_IN_PROD) {
logger.add(winston.transports.Console, {
colorize: true,
prettyPrint: true,
colorize: false,
prettyPrint: false,
});
}
} else if (IS_TEST) {
// Do not log anything when testing
} else {
} else if (!IS_TEST || IS_TEST && ENABLE_LOGS_IN_TEST) { // Do not log anything when testing unless specified
logger
.add(winston.transports.Console, {
colorize: true,

View File

@@ -4,10 +4,9 @@ import {
getUserInfo,
sendTxn as txnEmail,
} from './email';
import members from '../../controllers/api-v3/members';
import moment from 'moment';
import nconf from 'nconf';
import pushNotify from './pushNotifications';
import sendPushNotification from './pushNotifications';
import shared from '../../../../common' ;
const IS_PROD = nconf.get('IS_PROD');
@@ -91,7 +90,7 @@ api.createSubscription = async function createSubscription (data) {
data.user.purchased.txnCount++;
if (data.gift) {
members.sendMessage(data.user, data.gift.member, data.gift);
data.user.sendMessage(data.user, data.gift.member, data.gift);
let byUserName = getUserInfo(data.user, ['name']).name;
@@ -103,7 +102,7 @@ api.createSubscription = async function createSubscription (data) {
}
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedSubscription'), `${months} months - by ${byUserName}`);
sendPushNotification(data.gift.member, shared.i18n.t('giftedSubscription'), `${months} months - by ${byUserName}`);
}
}
@@ -164,7 +163,7 @@ api.buyGems = async function buyGems (data) {
let byUsername = getUserInfo(data.user, ['name']).name;
let gemAmount = data.gift.gems.amount || 20;
members.sendMessage(data.user, data.gift.member, data.gift);
data.user.sendMessage(data.user, data.gift.member, data.gift);
if (data.gift.member.preferences.emailNotifications.giftedGems !== false) {
txnEmail(data.gift.member, 'gifted-gems', [
{name: 'GIFTER', content: byUsername},
@@ -173,7 +172,7 @@ api.buyGems = async function buyGems (data) {
}
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
sendPushNotification(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
}
await data.gift.member.save();

View File

@@ -1,3 +1,5 @@
/* eslint-disable */
import _ from 'lodash';
import nconf from 'nconf';
import pushNotify from 'push-notify';
@@ -26,9 +28,12 @@ if (gcm) {
}
module.exports = function sendNotification (user, title, message, timeToLive = 15) {
if (!user) return;
return; // TODO push notifications are not currently enabled
_.each(user.pushDevices, pushDevice => {
if (!user) return;
let pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices;
_.each(pushDevices, pushDevice => {
switch (pushDevice.type) {
case 'android':
if (gcm) {

View File

@@ -231,7 +231,7 @@ schema.methods.removeTask = async function challengeRemoveTask (task) {
}, {multi: true}).exec();
};
// Unlink challenges tasks (and the challenge itself) from user
// Unlink challenges tasks (and the challenge itself) from user. TODO rename to 'leave'
schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep) {
let challengeId = this._id;
let findQuery = {

View File

@@ -517,7 +517,8 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
group.quest.progress.hp -= progress.up;
// TODO Create a party preferred language option so emits like this can be localized. Suggestion: Always display the English version too. Or, if English is not displayed to the players, at least include it in a new field in the chat object that's visible in the database - essential for admins when troubleshooting quests!
let playerAttack = `${user.profile.name} attacks ${quest.boss.name('en')} for ${progress.up.toFixed(1)} damage.`;
let bossAttack = nconf.get('CRON_SAFE_MODE') === 'true' ? `${quest.boss.name('en')} did not attack the party because it was asleep while maintenance was happening.` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`;
let bossAttack = nconf.get('CRON_SAFE_MODE') === 'true' || nconf.get('CRON_SEMI_SAFE_MODE') === 'true' ? `${quest.boss.name('en')} does not attack, because it respects the fact that there are some bugs\` \`post-maintenance and it doesn't want to hurt anyone unfairly. It will continue its rampage soon!` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`;
// TODO Consider putting the safe mode boss attack message in an ENV var
group.sendChat(`\`${playerAttack}\` \`${bossAttack}\``);
// If boss has Rage, increment Rage as well

View File

@@ -10,7 +10,10 @@ let Schema = mongoose.Schema;
let discriminatorOptions = {
discriminatorKey: 'type', // the key that distinguishes task types
};
let subDiscriminatorOptions = _.defaults(_.cloneDeep(discriminatorOptions), {_id: false});
let subDiscriminatorOptions = _.defaults(_.cloneDeep(discriminatorOptions), {
_id: false,
minimize: false,
});
export let tasksTypes = ['habit', 'daily', 'todo', 'reward'];
@@ -52,7 +55,7 @@ export let TaskSchema = new Schema({
time: {type: Date, required: true},
}],
}, _.defaults({
minimize: true, // So empty objects are returned
minimize: false, // So empty objects are returned
strict: true,
}, discriminatorOptions));

View File

@@ -1,58 +0,0 @@
var nconf = require('nconf');
var express = require('express');
var router = express.Router();
var _ = require('lodash');
var locals = require('../middlewares/api-v2/locals');
var i18n = require('../libs/api-v2/i18n');
var md = require('markdown-it')({
html: true,
});
const TOTAL_USER_COUNT = '1,100,000';
// -------- App --------
router.get('/', i18n.getUserLanguage, locals, function(req, res) {
if (!req.headers['x-api-user'] && !req.headers['x-api-key'] && !(req.session && req.session.userId))
return res.redirect('/static/front');
return res.render('index', {
title: 'Habitica | Your Life The Role Playing Game',
env: res.locals.habitrpg
});
});
// -------- Static Pages --------
var pages = ['front', 'privacy', 'terms', 'api', 'features', 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', 'old-news', 'press-kit', 'faq', 'overview', 'apps', 'clear-browser-data', 'merch', 'maintenance-info'];
_.each(pages, function(name){
router.get('/static/' + name, i18n.getUserLanguage, locals, function(req, res) {
res.render( 'static/' + name, {
env: res.locals.habitrpg,
md: md,
userCount: TOTAL_USER_COUNT
});
});
});
// -------- Social Media Sharing --------
var shareables = ['level-up','hatch-pet','raise-pet','unlock-quest','won-challenge','achievement'];
_.each(shareables, function(name){
router.get('/social/' + name, i18n.getUserLanguage, locals, function(req, res) {
res.render( 'social/' + name, {
env: res.locals.habitrpg,
md: md,
userCount: TOTAL_USER_COUNT
});
});
});
// --------- Redirects --------
router.get('/static/extensions', function(req, res) {
res.redirect('http://habitica.wikia.com/wiki/App_and_Extension_Integrations');
});
module.exports = router;

View File

@@ -183,11 +183,11 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
h5=env.t('changePass')
form(ng-submit='changeUser("password", passwordUpdates)', ng-show='user.auth.local', name='changePassword', novalidate)
.form-group
input.form-control(type='password', placeholder=env.t('oldPass'), ng-model='passwordUpdates.oldPassword', required)
input.form-control(type='password', placeholder=env.t('oldPass'), ng-model='passwordUpdates.password', required)
.form-group
input.form-control(type='password', placeholder=env.t('newPass'), ng-model='passwordUpdates.newPassword', required)
.form-group
input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='passwordUpdates.confirmNewPassword', required)
input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='passwordUpdates.confirmPassword', required)
input.btn.btn-default(type='submit', ng-disabled='changePassword.$invalid', value=env.t('submit'))

View File

@@ -88,7 +88,7 @@ script(type='text/ng-template', id='partials/options.social.hall.heroes.html')
span(ng-class='userAdminGlyphiconStyle(hero)')
span(ng-if='!hero.contributor.admin')
a.label.label-default(ng-class='userLevelStyle(hero)', ng-click='clickMember(hero._id, true)') {{hero.profile.name}}
td(ng-if='user.contributor.admin', ng-click='populateContributorInput(hero._id)').btn-link {{hero._id}}
td(ng-if='user.contributor.admin', ng-click='populateContributorInput(hero._id, $index)').btn-link {{hero._id}}
td {{hero.contributor.level}}
td {{hero.contributor.text}}
td

View File

@@ -98,8 +98,7 @@ footer.footer(ng-controller='FooterCtrl')
a.btn.btn-default(ng-click='addLevelsAndGold()') +Exp +GP +MP
a.btn.btn-default(ng-click='addOneLevel()') +1 Level
a.btn.btn-default(ng-click='addQuestProgress()' tooltip="+1000 to boss quests. 300 items to collection quests") Quest Progress Up
// TODO Re-enable after v3 prod testing
// a.btn.btn-default(ng-click='makeAdmin()') Make Admin
a.btn.btn-default(ng-click='makeAdmin()') Make Admin
a.btn.btn-default(ng-click='openModifyInventoryModal()') Modify Inventory
div(ng-init='deferredScripts()')

View File

@@ -3,7 +3,7 @@ div(ng-if='task._editing')
// Broken Challenge
.well(ng-if='task.challenge.broken')
div(ng-if='task.challenge.broken=="TASK_DELETED" || task.challenge.broken=="CHALLENGE_TASK_NOT_FOUND')
div(ng-if='task.challenge.broken=="TASK_DELETED" || task.challenge.broken=="CHALLENGE_TASK_NOT_FOUND"')
p=env.t('brokenTask')
p
a(ng-click='unlink(task, "keep")')=env.t('keepIt')

View File

@@ -1,4 +1,6 @@
mixin reward(item)
mixin reward(item, action)
- action = action || "buy(" + item + ")"
li.task.reward-item(popover-trigger='mouseenter', popover-placement='top', popover='{{::#{item}.notes()}}')&attributes(attributes)
// right-hand side control buttons
.task-meta-controls
@@ -8,8 +10,8 @@ mixin reward(item)
.task-controls.task-primary
input.reward.visuallyhidden(
type='checkbox',
ui-keypress='{13:"buy(#{item})"}')
a.money.btn-buy.item-btn(ng-class='::{highValue: #{item}.value >= 1000}', ng-click='::buy(#{item})')
ui-keypress='{13:"#{action}"}')
a.money.btn-buy.item-btn(ng-class='::{highValue: #{item}.value >= 1000}', ng-click='::#{action}')
span.shop_gold
span.reward-cost {{::#{item}.value}}
// main content
@@ -19,5 +21,5 @@ mixin reward(item)
ul.items.rewards(ng-if='main && list.type=="reward"')
+reward('item')(ng-repeat='item in itemStore')
+reward('healthPotion')
+reward('armoire')(ng-if='user.flags.armoireEnabled',
+reward('armoire', 'buyArmoire()')(ng-if='user.flags.armoireEnabled',
popover='{{armoire.notes(user, armoireCount(user.items.gear.owned))}}')