fix linting for server (except for length of apidoc)

This commit is contained in:
Matteo Pagliazzi
2019-10-10 20:11:50 +02:00
parent bcf7bcf03c
commit 8bae0223bb
84 changed files with 913 additions and 469 deletions
+6 -6
View File
@@ -12,7 +12,6 @@ import {
} from '../../libs/errors';
import * as passwordUtils from '../../libs/password';
import { sendTxn as sendTxnEmail } from '../../libs/email';
import { validatePasswordResetCodeAndFindUser, convertToBcrypt } from '../../libs/password';
import { encrypt } from '../../libs/encryption';
import {
loginRes,
@@ -125,7 +124,7 @@ api.loginLocal = {
headers: req.headers,
});
return loginRes(user, ...arguments);
return loginRes(user, req, res);
},
};
@@ -137,7 +136,7 @@ api.loginSocial = {
})],
url: '/user/auth/social',
async handler (req, res) {
return await loginSocial(req, res);
await loginSocial(req, res);
},
};
@@ -377,7 +376,7 @@ api.resetPasswordSetNewOne = {
method: 'POST',
url: '/user/auth/reset-password-set-new-one',
async handler (req, res) {
const user = await validatePasswordResetCodeAndFindUser(req.body.code);
const user = await passwordUtils.validatePasswordResetCodeAndFindUser(req.body.code);
const isValidCode = Boolean(user);
if (!isValidCode) throw new NotAuthorized(res.t('invalidPasswordResetCode'));
@@ -395,7 +394,7 @@ api.resetPasswordSetNewOne = {
}
// set new password and make sure it's using bcrypt for hashing
await convertToBcrypt(user, String(newPassword));
await passwordUtils.convertToBcrypt(user, String(newPassword));
user.auth.local.passwordResetCode = undefined; // Reset saved password reset code
await user.save();
@@ -418,7 +417,8 @@ api.deleteSocial = {
async handler (req, res) {
const { user } = res.locals;
const { network } = req.params;
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(supportedNetwork => supportedNetwork.key === network);
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS
.find(supportedNetwork => supportedNetwork.key === network);
if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork'));
if (!hasBackupAuth(user, network)) throw new NotAuthorized(res.t('cantDetachSocial'));
const unset = {
+28 -11
View File
@@ -413,8 +413,12 @@ api.getUserChallenges = {
User.findById(chal.leader).select(`${nameFields} backer contributor`).exec(),
Group.findById(chal.group).select(basicGroupFields).exec(),
]).then(populatedData => {
resChals[index].leader = populatedData[0] ? populatedData[0].toJSON({ minimize: true }) : null;
resChals[index].group = populatedData[1] ? populatedData[1].toJSON({ minimize: true }) : null;
resChals[index].leader = populatedData[0]
? populatedData[0].toJSON({ minimize: true })
: null;
resChals[index].group = populatedData[1]
? populatedData[1].toJSON({ minimize: true })
: null;
})));
res.respond(200, resChals);
@@ -460,12 +464,17 @@ api.getGroupChallenges = {
const challenges = await Challenge.find({ group: groupId })
.sort('-createdAt')
// .populate('leader', nameFields) // Only populate the leader as the group is implicit // see below why we're not using populate
// Only populate the leader as the group is implicit // see below why we're not using populate
// .populate('leader', nameFields)
.exec();
let resChals = challenges.map(challenge => challenge.toJSON());
resChals = _.orderBy(resChals, [challenge => challenge.categories.map(category => category.slug).includes('habitica_official')], ['desc']);
resChals = _.orderBy(
resChals,
[challenge => challenge.categories.map(category => category.slug).includes('habitica_official')],
['desc'],
);
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
await Promise.all(resChals.map((chal, index) => User
@@ -473,7 +482,9 @@ api.getGroupChallenges = {
.select(nameFields)
.exec()
.then(populatedLeader => {
resChals[index].leader = populatedLeader ? populatedLeader.toJSON({ minimize: true }) : null;
resChals[index].leader = populatedLeader
? populatedLeader.toJSON({ minimize: true })
: null;
})));
res.respond(200, resChals);
@@ -559,7 +570,8 @@ api.exportChallengeCsv = {
});
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
// In v2 this used the aggregation framework to run some computation on MongoDB but then iterated through all
// In v2 this used the aggregation framework to run some
// computation on MongoDB but then iterated through all
// results on the server so the perf difference isn't that big (hopefully)
const [members, tasks] = await Promise.all([
@@ -578,7 +590,8 @@ api.exportChallengeCsv = {
.exec(),
]);
let resArray = members.map(member => [member._id, member.profile.name, member.auth.local.username]);
let resArray = members
.map(member => [member._id, member.profile.name, member.auth.local.username]);
let lastUserId;
let index = -1;
@@ -594,8 +607,8 @@ api.exportChallengeCsv = {
return;
}
while (task.userId !== lastUserId) {
index++;
lastUserId = resArray[index][0]; // resArray[index][0] is an user id
index += 1;
lastUserId = [resArray[index]]; // resArray[index][0] is an user id
}
const streak = task.streak || 0;
@@ -603,8 +616,12 @@ api.exportChallengeCsv = {
resArray[index].push(`${task.type}:${task.text}`, task.value, task.notes, streak);
});
// The first row is going to be UUID name Task Value Notes repeated n times for the n challenge tasks
const challengeTasks = _.reduce(challenge.tasksOrder.toObject(), (result, array) => result.concat(array), []).sort();
// The first row is going to be UUID name Task Value Notes
// repeated n times for the n challenge tasks
const challengeTasks = _.reduce(
challenge.tasksOrder.toObject(),
(result, array) => result.concat(array), [],
).sort();
resArray.unshift(['UUID', 'Display Name', 'Username']);
_.times(challengeTasks.length, () => resArray[0].push('Task', 'Value', 'Notes', 'Streak'));
+11 -6
View File
@@ -110,7 +110,6 @@ api.postChat = {
async handler (req, res) {
const { user } = res.locals;
const { groupId } = req.params;
let chatUpdated;
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
req.sanitize('message').trim();
@@ -165,7 +164,8 @@ api.postChat = {
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
}
// prevent banned words being posted, except in private guilds/parties and in certain public guilds with specific topics
// prevent banned words being posted, except in private guilds/parties
// and in certain public guilds with specific topics
if (group.privacy === 'public' && !guildsAllowingBannedWords[group._id]) {
const matchedBadWords = getBannedWordsFromText(req.body.message);
if (matchedBadWords.length > 0) {
@@ -175,7 +175,9 @@ api.postChat = {
const chatRes = await Group.toJSONCleanChat(group, user);
const lastClientMsg = req.query.previousMsg;
chatUpdated = !!(lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg);
const chatUpdated = !!(
lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg
);
if (group.checkChatSpam(user)) {
throw new NotAuthorized(res.t('messageGroupChatSpam'));
@@ -449,7 +451,8 @@ api.seenChat = {
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
// Do not validate group existence, it doesn't really matter and make it works if the group gets deleted
// Do not validate group existence,
// it doesn't really matter and make it works if the group gets deleted
// let group = await Group.getGroup({user, groupId});
// if (!group) throw new NotFound(res.t('groupNotFound'));
@@ -476,7 +479,7 @@ api.seenChat = {
// Update the user version field manually,
// it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++;
user._v += 1;
await User.update({ _id: user._id }, update).exec();
res.respond(200, {});
@@ -529,7 +532,9 @@ api.deleteChat = {
const chatRes = await Group.toJSONCleanChat(group, user);
const lastClientMsg = req.query.previousMsg;
const chatUpdated = !!(lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg);
const chatUpdated = !!(
lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg
);
await Chat.remove({ _id: message._id }).exec();
+15 -8
View File
@@ -18,13 +18,17 @@ const api = {};
function walkContent (obj, lang) {
_.each(obj, (item, key, source) => {
if (_.isPlainObject(item) || _.isArray(item)) return walkContent(item, lang);
if (_.isFunction(item) && item.i18nLangFunc) source[key] = item(lang);
if (_.isPlainObject(item) || _.isArray(item)) {
walkContent(item, lang);
} else if (_.isFunction(item) && item.i18nLangFunc) {
source[key] = item(lang);
}
});
}
// After the getContent route is called the first time for a certain language
// the response is saved on disk and subsequentially served directly from there to reduce computation.
// the response is saved on disk and subsequentially served
// directly from there to reduce computation.
// Example: if `cachedContentResponses.en` is true it means that the response is cached
const cachedContentResponses = {};
@@ -43,18 +47,21 @@ async function saveContentToDisk (language, content) {
try {
cacheBeingWritten[language] = true;
await fs.stat(CONTENT_CACHE_PATH); // check if the directory exists, if it doesn't an error is thrown
// check if the directory exists, if it doesn't an error is thrown
await fs.stat(CONTENT_CACHE_PATH);
await fs.writeFile(`${CONTENT_CACHE_PATH}${language}.json`, content, 'utf8');
cacheBeingWritten[language] = false;
cachedContentResponses[language] = true;
} catch (err) {
if (err.code === 'ENOENT' && err.syscall === 'stat') { // the directory doesn't exists, create it and retry
// the directory doesn't exists, create it and retry
if (err.code === 'ENOENT' && err.syscall === 'stat') {
await fs.mkdir(CONTENT_CACHE_PATH);
return saveContentToDisk(language, content);
saveContentToDisk(language, content);
} else {
cacheBeingWritten[language] = false;
logger.error(err);
}
cacheBeingWritten[language] = false;
logger.error(err);
}
}
+30 -14
View File
@@ -25,7 +25,6 @@ import common from '../../../common';
import payments from '../../libs/payments/payments';
import stripePayments from '../../libs/payments/stripe';
import amzLib from '../../libs/payments/amazon';
import shared from '../../../common';
import apiError from '../../libs/apiError';
const MAX_EMAIL_INVITES_BY_USER = 200;
@@ -122,7 +121,7 @@ api.createGroup = {
group.balance = 1;
user.balance--;
user.balance -= 1;
user.guilds.push(group._id);
if (!user.achievements.joinedGuild) {
user.achievements.joinedGuild = true;
@@ -139,7 +138,8 @@ api.createGroup = {
const savedGroup = results[1];
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]); // doc.populate doesn't return a promise
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]);
// doc.populate doesn't return a promise
const response = savedGroup.toJSON();
// the leader is the authenticated user
response.leader = {
@@ -210,7 +210,7 @@ api.createGroupPlan = {
if (req.body.paymentType === 'Stripe') {
const token = req.body.id;
const gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
const sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
const sub = req.query.sub ? common.content.subscriptionBlocks[req.query.sub] : false;
const groupId = savedGroup._id;
const { email } = req.body;
const { headers } = req;
@@ -228,7 +228,9 @@ api.createGroupPlan = {
});
} else if (req.body.paymentType === 'Amazon') {
const { billingAgreementId } = req.body;
const sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
const sub = req.body.subscription
? common.content.subscriptionBlocks[req.body.subscription]
: false;
const { coupon } = req.body;
const groupId = savedGroup._id;
const { headers } = req;
@@ -244,7 +246,8 @@ api.createGroupPlan = {
}
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]); // doc.populate doesn't return a promise
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]);
// doc.populate doesn't return a promise
const response = savedGroup.toJSON();
// the leader is the authenticated user
response.leader = {
@@ -514,7 +517,9 @@ api.joinGroup = {
if (validationErrors) throw validationErrors;
// Works even if the user is not yet a member of the group
const group = await Group.getGroup({ user, groupId: req.params.groupId, optionalMembership: true }); // Do not fetch chat and work even if the user is not yet a member of the group
// Do not fetch chat and work even if the user is not yet a member of the group
const group = await Group
.getGroup({ user, groupId: req.params.groupId, optionalMembership: true });
if (!group) throw new NotFound(res.t('groupNotFound'));
let isUserInvited = false;
@@ -565,7 +570,8 @@ api.joinGroup = {
}
if (isUserInvited && group.type === 'guild') {
if (user.guilds.indexOf(group._id) !== -1) { // if user is already a member (party is checked previously)
// if user is already a member (party is checked previously)
if (user.guilds.indexOf(group._id) !== -1) {
throw new NotAuthorized(res.t('youAreAlreadyInGroup'));
}
user.guilds.push(group._id); // Add group to user's guilds
@@ -577,7 +583,9 @@ api.joinGroup = {
if (!isUserInvited) throw new NotAuthorized(res.t('messageGroupRequiresInvite'));
// @TODO: Review the need for this and if still needed, don't base this on memberCount
if (!group.hasNotCancelled() && group.memberCount === 0) group.leader = user._id; // If new user is only member -> set as leader
if (!group.hasNotCancelled() && group.memberCount === 0) {
group.leader = user._id; // If new user is only member -> set as leader
}
group.memberCount += 1;
@@ -600,7 +608,7 @@ api.joinGroup = {
if (!inviter.items.quests.basilist) {
inviter.items.quests.basilist = 0;
}
inviter.items.quests.basilist++;
inviter.items.quests.basilist += 1;
inviter.markModified('items.quests');
}
promises.push(inviter.save());
@@ -686,7 +694,9 @@ api.rejectGroupInvite = {
const hasPartyInvitation = removeFromArray(user.invitations.parties, { id: groupId });
if (hasPartyInvitation) {
user.invitations.party = user.invitations.parties.length > 0 ? user.invitations.parties[user.invitations.parties.length - 1] : {};
user.invitations.party = user.invitations.parties.length > 0
? user.invitations.parties[user.invitations.parties.length - 1]
: {};
user.markModified('invitations.party');
isUserInvited = true;
} else {
@@ -772,7 +782,10 @@ api.leaveGroup = {
throw new NotAuthorized(res.t('questLeaderCannotLeaveGroup'));
}
if (group.quest && group.quest.active && group.quest.members && group.quest.members[user._id]) {
if (
group.quest && group.quest.active
&& group.quest.members && group.quest.members[user._id]
) {
throw new NotAuthorized(res.t('cannotLeaveWhileActiveQuest'));
}
}
@@ -909,7 +922,9 @@ api.removeGroupMember = {
}
if (isInvited === 'party') {
removeFromArray(member.invitations.parties, { id: group._id });
member.invitations.party = member.invitations.parties.length > 0 ? member.invitations.parties[member.invitations.parties.length - 1] : {};
member.invitations.party = member.invitations.parties.length > 0
? member.invitations.parties[member.invitations.parties.length - 1]
: {};
member.markModified('invitations.party');
}
} else {
@@ -1064,7 +1079,8 @@ api.inviteToGroup = {
}
if (usernames) {
const usernameInvites = usernames.map(username => inviteByUserName(username, group, user, req, res));
const usernameInvites = usernames
.map(username => inviteByUserName(username, group, user, req, res));
const usernameResults = await Promise.all(usernameInvites);
results.push(...usernameResults);
}
+18 -10
View File
@@ -168,10 +168,9 @@ api.getHero = {
url: '/hall/heroes/:heroId',
middlewares: [authWithHeaders(), ensureAdmin],
async handler (req, res) {
let validationErrors;
req.checkParams('heroId', res.t('heroIdRequired')).notEmpty();
validationErrors = req.validationErrors();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const { heroId } = req.params;
@@ -256,22 +255,26 @@ api.updateHero = {
if (updateData.balance) hero.balance = updateData.balance;
// give them gems if they got an higher level
let newTier = updateData.contributor && updateData.contributor.level; // tier = level in this context
const oldTier = hero.contributor && hero.contributor.level || 0;
// tier = level in this context
let newTier = updateData.contributor && updateData.contributor.level;
const oldTier = (hero.contributor && hero.contributor.level) || 0;
if (newTier > oldTier) {
hero.flags.contributor = true;
let tierDiff = newTier - oldTier; // can be 2+ tier increases at once
while (tierDiff) {
hero.balance += gemsPerTier[newTier] / 4; // balance is in $
tierDiff--;
newTier--; // give them gems for the next tier down if they weren't aready that tier
tierDiff -= 1;
newTier -= 1; // give them gems for the next tier down if they weren't aready that tier
}
hero.addNotification('NEW_CONTRIBUTOR_LEVEL');
}
if (updateData.contributor) _.assign(hero.contributor, updateData.contributor);
if (updateData.purchased && updateData.purchased.ads) hero.purchased.ads = updateData.purchased.ads;
if (updateData.purchased && updateData.purchased.ads) {
hero.purchased.ads = updateData.purchased.ads;
}
// give them the Dragon Hydra pet if they're above level 6
if (hero.contributor.level >= 6) {
@@ -279,7 +282,8 @@ api.updateHero = {
hero.markModified('items.pets');
}
if (updateData.itemPath && updateData.itemVal && validateItemPath(updateData.itemPath)) {
_.set(hero, updateData.itemPath, castItemVal(updateData.itemPath, updateData.itemVal)); // Sanitization at 5c30944 (deemed unnecessary)
// Sanitization at 5c30944 (deemed unnecessary)
_.set(hero, updateData.itemPath, castItemVal(updateData.itemPath, updateData.itemVal));
}
if (updateData.auth && updateData.auth.blocked === true) {
@@ -290,8 +294,12 @@ api.updateHero = {
hero.auth.blocked = false;
}
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
if (updateData.flags && _.isBoolean(updateData.flags.chatShadowMuted)) hero.flags.chatShadowMuted = updateData.flags.chatShadowMuted;
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) {
hero.flags.chatRevoked = updateData.flags.chatRevoked;
}
if (updateData.flags && _.isBoolean(updateData.flags.chatShadowMuted)) {
hero.flags.chatShadowMuted = updateData.flags.chatShadowMuted;
}
const savedHero = await hero.save();
const heroJSON = savedHero.toJSON();
+10 -5
View File
@@ -259,7 +259,8 @@ api.getMemberAchievements = {
// Return a request handler for getMembersForGroup / getInvitesForGroup / getMembersForChallenge
// @TODO: This violates the Liskov substitution principle. We should create factory functions. See Webhooks for a good example
// @TODO: This violates the Liskov substitution principle.
// We should create factory functions. See Webhooks for a good example
function _getMembersForItem (type) {
// check for allowed `type`
if (['group-members', 'group-invites', 'challenge-members'].indexOf(type) === -1) {
@@ -288,7 +289,8 @@ function _getMembersForItem (type) {
challenge = await Challenge.findById(challengeId).select('_id type leader group').exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
// optionalMembership is set to true because even if you're not member of the group you may be able to access the challenge
// 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,
@@ -305,7 +307,8 @@ function _getMembersForItem (type) {
const query = {};
let fields = nameFields;
let addComputedStats = false; // add computes stats to the member info when items and stats are available
// add computes stats to the member info when items and stats are available
let addComputedStats = false;
if (type === 'challenge-members') {
query.challenges = challenge._id;
@@ -349,7 +352,8 @@ function _getMembersForItem (type) {
}
} else {
query['invitations.party.id'] = group._id; // group._id and not groupId because groupId could be === 'party'
// @TODO invitations are now stored like this: `'invitations.parties': []` Probably need a database index for it.
// @TODO invitations are now stored like this: `'invitations.parties': []`
// Probably need a database index for it.
if (req.query.includeAllPublicFields === 'true') {
fields = memberFields;
addComputedStats = true;
@@ -554,7 +558,8 @@ api.getChallengeMemberProgress = {
const challenge = await Challenge.findById(challengeId).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
// optionalMembership is set to true because even if you're not member of the group you may be able to access the challenge
// 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
const group = await Group.getGroup({
user, groupId: challenge.group, fields: '_id type privacy', optionalMembership: true,
+2 -1
View File
@@ -2,7 +2,8 @@ import { authWithHeaders } from '../../middlewares/auth';
const api = {};
// @TODO export this const, cannot export it from here because only routes are exported from controllers
// @TODO export this const, cannot export it
// from here because only routes are exported from controllers
const LAST_ANNOUNCEMENT_TITLE = 'SUPERNATURAL SKINS AND HAUNTED HAIR COLORS';
const worldDmg = { // @TODO
bailey: false,
@@ -43,7 +43,7 @@ api.readNotification = {
// Update the user version field manually,
// it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++;
user._v += 1;
await user.update({
$pull: { notifications: { id: req.params.notificationId } },
@@ -90,7 +90,7 @@ api.readNotifications = {
// Update the user version field manually,
// it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++;
user._v += 1;
res.respond(200, UserNotification.convertNotificationsToSafeJson(user.notifications));
},
@@ -140,7 +140,7 @@ api.seeNotification = {
// Update the user version field manually,
// it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++;
user._v += 1;
res.respond(200, notification);
},
+8 -3
View File
@@ -305,7 +305,9 @@ api.forceStart = {
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
if (!group.quest.key) throw new NotFound(res.t('questNotPending'));
if (group.quest.active) throw new NotAuthorized(res.t('questAlreadyUnderway'));
if (!(user._id === group.quest.leader || user._id === group.leader)) throw new NotAuthorized(res.t('questOrGroupLeaderOnlyStartQuest'));
if (!(user._id === group.quest.leader || user._id === group.leader)) {
throw new NotAuthorized(res.t('questOrGroupLeaderOnlyStartQuest'));
}
group.markModified('quest');
@@ -352,7 +354,8 @@ api.cancelQuest = {
async handler (req, res) {
// Cancel a quest BEFORE it has begun (i.e., in the invitation stage)
// Quest scroll has not yet left quest owner's inventory so no need to return it.
// Do not wipe quest progress for members because they'll want it to be applied to the next quest that's started.
// Do not wipe quest progress for members because they'll
// want it to be applied to the next quest that's started.
const { user } = res.locals;
const { groupId } = req.params;
@@ -366,7 +369,9 @@ api.cancelQuest = {
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
if (!group.quest.key) throw new NotFound(res.t('questInvitationDoesNotExist'));
if (user._id !== group.leader && group.quest.leader !== user._id) throw new NotAuthorized(res.t('onlyLeaderCancelQuest'));
if (user._id !== group.leader && group.quest.leader !== user._id) {
throw new NotAuthorized(res.t('onlyLeaderCancelQuest'));
}
if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest'));
const questName = questScrolls[group.quest.key].text('en');
+1 -1
View File
@@ -227,7 +227,7 @@ api.deleteTag = {
// Update the user version field manually,
// it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++;
user._v += 1;
// Remove from all the tasks TODO test
await Tasks.Task.update({
+71 -24
View File
@@ -383,12 +383,23 @@ api.getTask = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
const challenge = await Challenge.find({ _id: task.challenge.id }).select('leader').exec();
if (!challenge || (user.challenges.indexOf(task.challenge.id) === -1 && challenge.leader !== user._id && !user.contributor.admin)) { // eslint-disable-line no-extra-parens
if (
!challenge
|| (
user.challenges.indexOf(task.challenge.id) === -1
&& challenge.leader !== user._id
&& !user.contributor.admin
)
) { // eslint-disable-line no-extra-parens
throw new NotFound(res.t('taskNotFound'));
}
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound'));
}
@@ -451,16 +462,21 @@ api.updateTask = {
group = await Group.getGroup({ user, groupId: task.group.id, fields });
if (!group) throw new NotFound(res.t('groupNotFound'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound'));
}
const oldCheckList = task.checklist;
// we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances?
// we have to convert task to an object because otherwise things
// don't get merged correctly. Bad for performances?
const [updatedTaskObj] = common.ops.updateTask(task.toObject(), req);
// Sanitize differently user tasks linked to a challenge
let sanitizedObj;
@@ -476,7 +492,8 @@ api.updateTask = {
_.assign(task, sanitizedObj);
// console.log(task.modifiedPaths(), task.toObject().repeat === tep)
// repeat is always among modifiedPaths because mongoose changes the other of the keys when using .toObject()
// repeat is always among modifiedPaths because mongoose changes
// the other of the keys when using .toObject()
// see https://github.com/Automattic/mongoose/issues/2749
task.group.approval.required = false;
@@ -585,7 +602,8 @@ api.scoreTask = {
const managers = await User.find({ _id: managerIds }, 'notifications preferences').exec(); // Use this method so we can get access to notifications
// @TODO: we can use the User.pushNotification function because we need to ensure notifications are translated
// @TODO: we can use the User.pushNotification function because
// we need to ensure notifications are translated
const managerPromises = [];
managers.forEach(manager => {
manager.addNotification('GROUP_TASK_APPROVAL', {
@@ -594,7 +612,8 @@ api.scoreTask = {
taskName: task.text,
}, manager.preferences.language),
groupId: group._id,
taskId: task._id, // user task id, used to match the notification when the task is approved
// user task id, used to match the notification when the task is approved
taskId: task._id,
userId: user._id,
groupTaskId: task.group.taskId, // the original task id
direction,
@@ -612,7 +631,8 @@ api.scoreTask = {
const wasCompleted = task.completed;
const [delta] = common.ops.scoreTask({ task, user, direction }, req);
// Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results)
// Drop system (don't run on the client,
// as it would only be discarded since ops are sent to the API, not the results)
if (direction === 'up') common.fns.randomDrop(user, { task, delta }, req, res.analytics);
// If a todo was completed or uncompleted move it in or out of the user.tasksOrder.todos list
@@ -626,7 +646,11 @@ api.scoreTask = {
$pull: { 'tasksOrder.todos': task._id },
}).exec();
// user.tasksOrder.todos.pull(task._id);
} else if (wasCompleted && !task.completed && user.tasksOrder.todos.indexOf(task._id) === -1) {
} else if (
wasCompleted
&& !task.completed
&& user.tasksOrder.todos.indexOf(task._id) === -1
) {
taskOrderPromise = user.update({
$push: { 'tasksOrder.todos': task._id },
}).exec();
@@ -649,7 +673,9 @@ api.scoreTask = {
}).exec();
if (groupTask) {
const groupDelta = groupTask.group.assignedUsers ? delta / groupTask.group.assignedUsers.length : delta;
const groupDelta = groupTask.group.assignedUsers
? delta / groupTask.group.assignedUsers.length
: delta;
await groupTask.scoreChallengeTask(groupDelta, direction);
}
} catch (e) {
@@ -675,7 +701,8 @@ api.scoreTask = {
});
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
// 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 {
const chalTask = await Tasks.Task.findOne({
_id: task.challenge.taskId,
@@ -763,7 +790,7 @@ api.moveTask = {
// Update the user version field manually,
// it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++;
user._v += 1;
res.respond(200, order);
},
@@ -811,11 +838,15 @@ api.addChecklistItem = {
const fields = requiredGroupFields.concat(' managers');
group = await Group.getGroup({ user, groupId: task.group.id, fields });
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound'));
}
@@ -927,11 +958,15 @@ api.updateChecklistItem = {
group = await Group.getGroup({ user, groupId: task.group.id, fields });
if (!group) throw new NotFound(res.t('groupNotFound'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound'));
}
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
@@ -992,11 +1027,15 @@ api.removeChecklistItem = {
group = await Group.getGroup({ user, groupId: task.group.id, fields });
if (!group) throw new NotFound(res.t('groupNotFound'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound'));
}
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
@@ -1310,15 +1349,23 @@ api.deleteTask = {
if (!group) throw new NotFound(res.t('groupNotFound'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
await group.removeTask(task);
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound'));
} else if (task.userId && task.challenge.id && !task.challenge.broken) {
throw new NotAuthorized(res.t('cantDeleteChallengeTasks'));
} else if (task.group.id && task.group.assignedUsers.indexOf(user._id) !== -1 && !task.group.broken) {
} else if (
task.group.id
&& task.group.assignedUsers.indexOf(user._id) !== -1
&& !task.group.broken
) {
throw new NotAuthorized(res.t('cantDeleteAssignedGroupTasks'));
}
@@ -1332,7 +1379,7 @@ api.deleteTask = {
// Update the user version field manually,
// it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
if (!challenge) user._v++;
if (!challenge) user._v += 1;
await Promise.all([taskOrderUpdate, task.remove()]);
} else {
@@ -18,7 +18,8 @@ import apiError from '../../../libs/apiError';
const requiredGroupFields = '_id leader tasksOrder name';
// @TODO: abstract to task lib
const types = Tasks.tasksTypes.map(type => `${type}s`);
types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future
// _allCompletedTodos is currently in BETA and is likely to be removed in future
types.push('completedTodos', '_allCompletedTodos');
function canNotEditTasks (group, user, assignedUserId) {
const isNotGroupLeader = group.leader !== user._id;
@@ -96,7 +97,11 @@ api.getGroupTasks = {
const { user } = res.locals;
const group = await Group.getGroup({ user, groupId: req.params.groupId, fields: requiredGroupFields });
const group = await Group.getGroup({
user,
groupId: req.params.groupId,
fields: requiredGroupFields,
});
if (!group) throw new NotFound(res.t('groupNotFound'));
const tasks = await getTasks(req, res, { user, group });
@@ -142,7 +147,11 @@ api.groupMoveTask = {
if (task.type === 'todo' && task.completed) throw new BadRequest(res.t('cantMoveCompletedTodo'));
const group = await Group.getGroup({ user, groupId: task.group.id, fields: requiredGroupFields });
const group = await Group.getGroup({
user,
groupId: task.group.id,
fields: requiredGroupFields,
});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
+15 -5
View File
@@ -123,7 +123,10 @@ api.getBuyList = {
// return text and notes strings
_.each(list, item => {
_.each(item, (itemPropVal, itemPropKey) => {
if (_.isFunction(itemPropVal) && itemPropVal.i18nLangFunc) item[itemPropKey] = itemPropVal(req.language);
if (
_.isFunction(itemPropVal)
&& itemPropVal.i18nLangFunc
) item[itemPropKey] = itemPropVal(req.language);
});
});
@@ -166,7 +169,10 @@ api.getInAppRewardsList = {
// return text and notes strings
_.each(list, item => {
_.each(item, (itemPropVal, itemPropKey) => {
if (_.isFunction(itemPropVal) && itemPropVal.i18nLangFunc) item[itemPropKey] = itemPropVal(req.language);
if (
_.isFunction(itemPropVal)
&& itemPropVal.i18nLangFunc
) item[itemPropKey] = itemPropVal(req.language);
});
});
@@ -455,7 +461,6 @@ api.buy = {
async handler (req, res) {
const { user } = res.locals;
let buyRes;
// @TODO: Remove this when mobile passes type in body
const type = req.params.key;
if (buySpecialKeys.indexOf(type) !== -1) {
@@ -470,7 +475,7 @@ api.buy = {
let quantity = 1;
if (req.body.quantity) quantity = req.body.quantity;
req.quantity = quantity;
buyRes = common.ops.buy(user, req, res.analytics);
const buyRes = common.ops.buy(user, req, res.analytics);
await user.save();
res.respond(200, ...buyRes);
@@ -1003,7 +1008,12 @@ api.userPurchaseHourglass = {
const { user } = res.locals;
const quantity = req.body.quantity || 1;
if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(res.t('invalidQuantity'), req.language);
const purchaseHourglassRes = common.ops.buy(user, req, res.analytics, { quantity, hourglass: true });
const purchaseHourglassRes = common.ops.buy(
user,
req,
res.analytics,
{ quantity, hourglass: true },
);
await user.save();
res.respond(200, ...purchaseHourglassRes);
},