Merge branch 'develop' of https://github.com/HabitRPG/habitica into negue/flagpm

# Conflicts:
#	website/client/components/chat/chatCard.vue
#	website/client/components/chat/chatMessages.vue
#	website/common/locales/en/messages.json
#	website/server/libs/slack.js
This commit is contained in:
negue
2018-09-22 19:18:08 +02:00
1128 changed files with 51381 additions and 42173 deletions
+11 -162
View File
@@ -11,61 +11,22 @@ import {
NotFound,
} from '../../libs/errors';
import * as passwordUtils from '../../libs/password';
import logger from '../../libs/logger';
import { model as User } from '../../models/user';
import { model as Group } from '../../models/group';
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
import { sendTxn as sendTxnEmail } from '../../libs/email';
import { decrypt, encrypt } from '../../libs/encryption';
import { send as sendEmail } from '../../libs/email';
import pusher from '../../libs/pusher';
import common from '../../../common';
import { validatePasswordResetCodeAndFindUser, convertToBcrypt} from '../../libs/password';
import { encrypt } from '../../libs/encryption';
import * as authLib from '../../libs/auth';
const BASE_URL = nconf.get('BASE_URL');
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
const USERNAME_LENGTH_MIN = 1;
const USERNAME_LENGTH_MAX = 20;
let api = {};
// When the user signed up after having been invited to a group, invite them automatically to the group
async function _handleGroupInvitation (user, invite) {
// wrapping the code in a try because we don't want it to prevent the user from signing up
// that's why errors are not translated
try {
let {sentAt, id: groupId, inviter} = JSON.parse(decrypt(invite));
// check that the invite has not expired (after 7 days)
if (sentAt && moment().subtract(7, 'days').isAfter(sentAt)) {
let err = new Error('Invite expired.');
err.privateData = invite;
throw err;
}
let group = await Group.getGroup({user, optionalMembership: true, groupId, fields: 'name type'});
if (!group) throw new NotFound('Group not found.');
if (group.type === 'party') {
user.invitations.party = {id: group._id, name: group.name, inviter};
user.invitations.parties.push(user.invitations.party);
} else {
user.invitations.guilds.push({id: group._id, name: group.name, inviter});
}
// award the inviter with 'Invited a Friend' achievement
inviter = await User.findById(inviter);
if (!inviter.achievements.invitedFriend) {
inviter.achievements.invitedFriend = true;
inviter.addNotification('INVITED_FRIEND_ACHIEVEMENT');
await inviter.save();
}
} catch (err) {
logger.error(err);
}
}
function hasBackupAuth (user, networkToRemove) {
if (user.auth.local.username) {
return true;
@@ -78,6 +39,8 @@ function hasBackupAuth (user, networkToRemove) {
return hasAlternateNetwork;
}
/* NOTE this route has also an API v4 version */
/**
* @api {post} /api/v3/user/auth/local/register Register
* @apiDescription Register a new user with email, login name, and password or attach local auth to a social user
@@ -98,111 +61,7 @@ api.registerLocal = {
})],
url: '/user/auth/local/register',
async handler (req, res) {
let existingUser = res.locals.user; // If adding local auth to social user
req.checkBody({
email: {
notEmpty: {errorMessage: res.t('missingEmail')},
isEmail: {errorMessage: res.t('notAnEmail')},
},
username: {
notEmpty: {errorMessage: res.t('missingUsername')},
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')},
// TODO use the constants in the error message above
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')},
},
password: {
notEmpty: {errorMessage: res.t('missingPassword')},
equals: {options: [req.body.confirmPassword], errorMessage: res.t('passwordConfirmationMatch')},
},
});
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let { email, username, password } = req.body;
// Get the lowercase version of username to check that we do not have duplicates
// So we can search for it in the database and then reject the choosen username if 1 or more results are found
email = email.toLowerCase();
username = username.trim();
let lowerCaseUsername = username.toLowerCase();
// Search for duplicates using lowercase version of username
let user = await User.findOne({$or: [
{'auth.local.email': email},
{'auth.local.lowerCaseUsername': lowerCaseUsername},
]}, {'auth.local': 1}).exec();
if (user) {
if (email === user.auth.local.email) throw new NotAuthorized(res.t('emailTaken'));
// Check that the lowercase username isn't already used
if (lowerCaseUsername === user.auth.local.lowerCaseUsername) throw new NotAuthorized(res.t('usernameTaken'));
}
let hashed_password = await passwordUtils.bcryptHash(password); // eslint-disable-line camelcase
let newUser = {
auth: {
local: {
username,
lowerCaseUsername,
email,
hashed_password, // eslint-disable-line camelcase,
passwordHashMethod: 'bcrypt',
},
},
preferences: {
language: req.language,
},
};
if (existingUser) {
let hasSocialAuth = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(network => {
if (existingUser.auth.hasOwnProperty(network.key)) {
return existingUser.auth[network.key].id;
}
});
if (!hasSocialAuth) throw new NotAuthorized(res.t('onlySocialAttachLocal'));
existingUser.auth.local = newUser.auth.local;
newUser = existingUser;
} else {
newUser = new User(newUser);
newUser.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
}
// we check for partyInvite for backward compatibility
if (req.query.groupInvite || req.query.partyInvite) {
await _handleGroupInvitation(newUser, req.query.groupInvite || req.query.partyInvite);
}
let savedUser = await newUser.save();
if (existingUser) {
res.respond(200, savedUser.toJSON().auth.local); // We convert to toJSON to hide private fields
} else {
let userJSON = savedUser.toJSON();
userJSON.newUser = true;
res.respond(201, userJSON);
}
// Clean previous email preferences and send welcome email
EmailUnsubscription
.remove({email: savedUser.auth.local.email})
.then(() => {
if (!existingUser) sendTxnEmail(savedUser, 'welcome');
});
if (!existingUser) {
res.analytics.track('register', {
category: 'acquisition',
type: 'local',
gaLabel: 'local',
uuid: savedUser._id,
headers: req.headers,
user: savedUser,
});
}
return null;
await authLib.registerLocal(req, res, { isV3: true });
},
};
@@ -394,9 +253,7 @@ api.loginSocial = {
*/
api.pusherAuth = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/auth/pusher',
async handler (req, res) {
let user = res.locals.user;
@@ -464,9 +321,7 @@ api.pusherAuth = {
**/
api.updateUsername = {
method: 'PUT',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/auth/update-username',
async handler (req, res) {
let user = res.locals.user;
@@ -476,7 +331,7 @@ api.updateUsername = {
notEmpty: {errorMessage: res.t('missingPassword')},
},
username: {
notEmpty: { errorMessage: res.t('missingUsername') },
notEmpty: {errorMessage: res.t('missingUsername')},
},
});
@@ -520,9 +375,7 @@ api.updateUsername = {
**/
api.updatePassword = {
method: 'PUT',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/auth/update-password',
async handler (req, res) {
let user = res.locals.user;
@@ -632,9 +485,7 @@ api.resetPassword = {
*/
api.updateEmail = {
method: 'PUT',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/auth/update-email',
async handler (req, res) {
let user = res.locals.user;
@@ -721,9 +572,7 @@ api.resetPasswordSetNewOne = {
api.deleteSocial = {
method: 'DELETE',
url: '/user/auth/social/:network',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let network = req.params.network;
+15 -33
View File
@@ -184,9 +184,7 @@ let api = {};
api.createChallenge = {
method: 'POST',
url: '/challenges',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -235,9 +233,7 @@ api.createChallenge = {
api.joinChallenge = {
method: 'POST',
url: '/challenges/:challengeId/join',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -251,7 +247,7 @@ api.joinChallenge = {
if (challenge.isMember(user)) throw new NotAuthorized(res.t('userAlreadyInChallenge'));
let group = await Group.getGroup({user, groupId: challenge.group, fields: basicGroupFields, optionalMembership: true});
if (!group || !challenge.hasAccess(user, group)) throw new NotFound(res.t('challengeNotFound'));
if (!group || !challenge.canJoin(user, group)) throw new NotFound(res.t('challengeNotFound'));
challenge.memberCount += 1;
@@ -294,9 +290,7 @@ api.joinChallenge = {
api.leaveChallenge = {
method: 'POST',
url: '/challenges/:challengeId/leave',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let keep = req.body.keep === 'remove-all' ? 'remove-all' : 'keep-all';
@@ -329,7 +323,7 @@ api.leaveChallenge = {
};
/**
* @api {get} /api/v3/challenges/user Get challenges for a user.
* @api {get} /api/v3/challenges/user Get challenges for a user
* @apiName GetUserChallenges
* @apiGroup Challenge
* @apiDescription Get challenges the user has access to. Includes public challenges, challenges belonging to the user's group, and challenges the user has already joined.
@@ -345,9 +339,7 @@ api.leaveChallenge = {
api.getUserChallenges = {
method: 'GET',
url: '/challenges/user',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
const CHALLENGES_PER_PAGE = 10;
const page = req.query.page;
@@ -449,7 +441,7 @@ api.getGroupChallenges = {
method: 'GET',
url: '/challenges/groups/:groupId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
userFieldsToInclude: ['_id', 'party', 'guilds'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -463,10 +455,10 @@ api.getGroupChallenges = {
if (groupId === 'party') groupId = user.party._id;
if (groupId === 'habitrpg') groupId = TAVERN_ID;
let group = await Group.getGroup({user, groupId});
const group = await Group.getGroup({ user, groupId });
if (!group) throw new NotFound(res.t('groupNotFound'));
let challenges = await Challenge.find({group: groupId})
const challenges = await Challenge.find({ group: groupId })
.sort('-createdAt')
// .populate('leader', nameFields) // Only populate the leader as the group is implicit
.exec();
@@ -508,9 +500,7 @@ api.getGroupChallenges = {
api.getChallenge = {
method: 'GET',
url: '/challenges/:challengeId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
@@ -640,7 +630,7 @@ api.exportChallengeCsv = {
};
/**
* @api {put} /api/v3/challenges/:challengeId Update the name, description, or leader of a challenge.
* @api {put} /api/v3/challenges/:challengeId Update the name, description, or leader of a challenge
*
* @apiName UpdateChallenge
* @apiGroup Challenge
@@ -664,9 +654,7 @@ api.exportChallengeCsv = {
api.updateChallenge = {
method: 'PUT',
url: '/challenges/:challengeId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
@@ -708,9 +696,7 @@ api.updateChallenge = {
api.deleteChallenge = {
method: 'DELETE',
url: '/challenges/:challengeId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -755,9 +741,7 @@ api.deleteChallenge = {
api.selectChallengeWinner = {
method: 'POST',
url: '/challenges/:challengeId/selectWinner/:winnerId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -806,9 +790,7 @@ api.selectChallengeWinner = {
api.cloneChallenge = {
method: 'POST',
url: '/challenges/:challengeId/clone',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
+9 -22
View File
@@ -1,7 +1,7 @@
import { authWithHeaders } from '../../middlewares/auth';
import { model as Group } from '../../models/group';
import { model as User } from '../../models/user';
import { model as Chat } from '../../models/chat';
import { chatModel as Chat } from '../../models/message';
import {
BadRequest,
NotFound,
@@ -67,9 +67,7 @@ function textContainsBannedSlur (message) {
api.getChat = {
method: 'GET',
url: '/groups/:groupId/chat',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -108,9 +106,7 @@ function getBannedWordsFromText (message) {
api.postChat = {
method: 'POST',
url: '/groups/:groupId/chat',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let groupId = req.params.groupId;
@@ -164,6 +160,7 @@ api.postChat = {
}
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.privacy !== 'private' && user.flags.chatRevoked) {
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
}
@@ -231,9 +228,7 @@ api.postChat = {
api.likeChat = {
method: 'POST',
url: '/groups/:groupId/chat/:chatId/like',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let groupId = req.params.groupId;
@@ -290,9 +285,7 @@ api.likeChat = {
api.flagChat = {
method: 'POST',
url: '/groups/:groupId/chat/:chatId/flag',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
const chatReporter = chatReporterFactory('Group', req, res);
const message = await chatReporter.flag();
@@ -321,9 +314,7 @@ api.flagChat = {
api.clearChatFlags = {
method: 'Post',
url: '/groups/:groupId/chat/:chatId/clearflags',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let groupId = req.params.groupId;
@@ -393,9 +384,7 @@ api.clearChatFlags = {
api.seenChat = {
method: 'POST',
url: '/groups/:groupId/chat/seen',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let groupId = req.params.groupId;
@@ -461,9 +450,7 @@ api.seenChat = {
api.deleteChat = {
method: 'DELETE',
url: '/groups/:groupId/chat/:chatId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let groupId = req.params.groupId;
+10 -17
View File
@@ -4,10 +4,11 @@ import {
authWithSession,
} from '../../middlewares/auth';
import { ensureSudo } from '../../middlewares/ensureAccessRight';
import { model as Coupon } from '../../models/coupon';
import _ from 'lodash';
import * as couponsLib from '../../libs/coupons';
import couponCode from 'coupon-code';
import apiError from '../../libs/apiError';
import { model as Coupon } from '../../models/coupon';
let api = {};
@@ -68,9 +69,7 @@ api.getCoupons = {
api.generateCoupons = {
method: 'POST',
url: '/coupons/generate/:event',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
}), ensureSudo],
middlewares: [authWithHeaders(), ensureSudo],
async handler (req, res) {
req.checkParams('event', apiError('eventRequired')).notEmpty();
req.checkQuery('count', apiError('countRequired')).notEmpty().isNumeric();
@@ -83,6 +82,8 @@ api.generateCoupons = {
},
};
/* NOTE this route has also an API v4 version */
/**
* @api {post} /api/v3/coupons/enter/:code Redeem a coupon code
* @apiName RedeemCouponCode
@@ -95,19 +96,12 @@ api.generateCoupons = {
api.enterCouponCode = {
method: 'POST',
url: '/coupons/enter/:code',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
await Coupon.apply(user, req, req.params.code);
res.respond(200, user);
const user = res.locals.user;
await couponsLib.enterCode(req, res, user);
const userToJSON = await user.toJSONWithInbox();
res.respond(200, userToJSON);
},
};
@@ -125,7 +119,6 @@ api.validateCoupon = {
url: '/coupons/validate/:code',
middlewares: [authWithHeaders({
optional: true,
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
+43 -49
View File
@@ -109,9 +109,7 @@ let api = {};
api.createGroup = {
method: 'POST',
url: '/groups',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let group = new Group(Group.sanitize(req.body));
@@ -182,9 +180,7 @@ api.createGroup = {
api.createGroupPlan = {
method: 'POST',
url: '/groups/create-plan',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let group = new Group(Group.sanitize(req.body.groupToCreate));
@@ -196,7 +192,6 @@ api.createGroupPlan = {
// @TODO: Change message
if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate'));
group.memberCount = await User.count({ $or: [{ 'party._id': group._id }, { guilds: group._id }] }).exec();
group.leader = user._id;
user.guilds.push(group._id);
@@ -294,9 +289,7 @@ api.createGroupPlan = {
api.getGroups = {
method: 'GET',
url: '/groups',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -385,7 +378,7 @@ api.getGroup = {
method: 'GET',
url: '/groups/:groupId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
userFieldsToInclude: ['_id', 'party', 'guilds', 'contributor'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -444,9 +437,7 @@ api.getGroup = {
api.updateGroup = {
method: 'PUT',
url: '/groups/:groupId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -509,9 +500,7 @@ api.updateGroup = {
api.joinGroup = {
method: 'POST',
url: '/groups/:groupId/join',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let inviter;
@@ -588,12 +577,8 @@ api.joinGroup = {
}
if (!isUserInvited) throw new NotAuthorized(res.t('messageGroupRequiresInvite'));
if (group.memberCount === 0) group.leader = user._id; // If new user is only member -> set as leader
if (group.hasNotCancelled()) {
await payments.addSubToGroupUser(user, group);
await group.updateGroupPlan();
}
// @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
group.memberCount += 1;
@@ -638,6 +623,11 @@ api.joinGroup = {
promises = await Promise.all(promises);
if (group.hasNotCancelled()) {
await payments.addSubToGroupUser(user, group);
await group.updateGroupPlan();
}
let response = await Group.toJSONCleanChat(promises[0], user);
let leader = await User.findById(response.leader).select(nameFields).exec();
if (leader) {
@@ -682,9 +672,7 @@ api.joinGroup = {
api.rejectGroupInvite = {
method: 'POST',
url: '/groups/:groupId/reject-invite',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -759,9 +747,7 @@ function _removeMessagesFromMember (member, groupId) {
api.leaveGroup = {
method: 'POST',
url: '/groups/:groupId/leave',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
@@ -790,7 +776,6 @@ api.leaveGroup = {
}
await group.leave(user, req.query.keep, req.body.keepChallenges);
if (group.hasNotCancelled()) await group.updateGroupPlan(true);
_removeMessagesFromMember(user, group._id);
await user.save();
@@ -804,6 +789,7 @@ api.leaveGroup = {
await payments.cancelGroupSubscriptionForUser(user, group);
}
if (group.hasNotCancelled()) await group.updateGroupPlan(true);
res.respond(200, {});
},
};
@@ -848,9 +834,7 @@ function _sendMessageToRemoved (group, removedUser, message, isInGroup) {
api.removeGroupMember = {
method: 'POST',
url: '/groups/:groupId/removeMember/:memberId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -892,10 +876,6 @@ api.removeGroupMember = {
if (isInGroup) {
group.memberCount -= 1;
if (group.hasNotCancelled()) {
await group.updateGroupPlan(true);
await payments.cancelGroupSubscriptionForUser(member, group, true);
}
if (group.quest && group.quest.leader === member._id) {
group.quest.key = undefined;
@@ -944,6 +924,12 @@ api.removeGroupMember = {
member.save(),
group.save(),
]);
if (isInGroup && group.hasNotCancelled()) {
await group.updateGroupPlan(true);
await payments.cancelGroupSubscriptionForUser(member, group, true);
}
res.respond(200, {});
},
};
@@ -958,6 +944,11 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
throw new BadRequest(res.t('cannotInviteSelfToGroup'));
}
const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite);
if (objections.length > 0) {
throw new NotAuthorized(res.t(objections[0], { userId: uuid, username: userToInvite.profile.name}));
}
if (group.type === 'guild') {
if (_.includes(userToInvite.guilds, group._id)) {
throw new NotAuthorized(res.t('userAlreadyInGroup', { userId: uuid, username: userToInvite.profile.name}));
@@ -1160,6 +1151,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
* @apiError (401) {NotAuthorized} UserAlreadyInvited The user has already been invited to the group.
* @apiError (401) {NotAuthorized} UserAlreadyInGroup The user is already a member of the group.
* @apiError (401) {NotAuthorized} CannotInviteWhenMuted You cannot invite anyone to a guild or party because your chat privileges have been revoked.
* @apiError (401) {NotAuthorized} NotAuthorizedToSendMessageToThisUser You can't send a message to this player because they have chosen to block messages.
*
* @apiUse GroupNotFound
* @apiUse UserNotFound
@@ -1168,9 +1160,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
api.inviteToGroup = {
method: 'POST',
url: '/groups/:groupId/invite',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -1209,6 +1199,16 @@ api.inviteToGroup = {
results.push(...emailResults);
}
let analyticsObject = {
uuid: user._id,
hitType: 'event',
category: 'behavior',
groupType: group.type,
headers: req.headers,
};
res.analytics.track('group invite', analyticsObject);
res.respond(200, results);
},
};
@@ -1233,9 +1233,7 @@ api.inviteToGroup = {
api.addGroupManager = {
method: 'POST',
url: '/groups/:groupId/add-manager',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let managerId = req.body.managerId;
@@ -1284,9 +1282,7 @@ api.addGroupManager = {
api.removeGroupManager = {
method: 'POST',
url: '/groups/:groupId/remove-manager',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let managerId = req.body.managerId;
@@ -1339,9 +1335,7 @@ api.removeGroupManager = {
api.getGroupPlans = {
method: 'GET',
url: '/group-plans',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
+3 -6
View File
@@ -61,9 +61,7 @@ let api = {};
api.getPatrons = {
method: 'GET',
url: '/hall/patrons',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkQuery('page').optional().isInt({min: 0}, apiError('queryPageInteger'));
@@ -123,9 +121,7 @@ api.getPatrons = {
api.getHeroes = {
method: 'GET',
url: '/hall/heroes',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let heroes = await User
.find({
@@ -273,6 +269,7 @@ api.updateHero = {
if (updateData.auth && updateData.auth.blocked === false) {
hero.auth.blocked = false;
}
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
let savedHero = await hero.save();
+1 -1
View File
@@ -16,7 +16,7 @@ function geti18nBrowserScript (language) {
availableLanguages,
language,
strings: translations[langCode],
momentLang: momentLangs[language.momentLangCode],
momentLang: momentLangs[langCode],
})};
})()`;
}
@@ -0,0 +1,29 @@
import { authWithHeaders } from '../../middlewares/auth';
import * as inboxLib from '../../libs/inbox';
let api = {};
/* NOTE most inbox routes are either in the user or members controller */
/**
* @api {get} /api/v3/inbox/messages Get inbox messages for a user
* @apiName GetInboxMessages
* @apiGroup Inbox
* @apiDescription Get inbox messages for a user
*
* @apiSuccess {Array} data An array of inbox messages
*/
api.getInboxMessages = {
method: 'GET',
url: '/inbox/messages',
middlewares: [authWithHeaders()],
async handler (req, res) {
const user = res.locals.user;
const userInbox = await inboxLib.getUserInbox(user);
res.respond(200, userInbox);
},
};
module.exports = api;
+143 -22
View File
@@ -33,6 +33,62 @@ let api = {};
*
* @apiSuccess {Object} data The member object
*
* @apiSuccess (Object) data.inbox Basic information about person's inbox
* @apiSuccess (Object) data.stats Includes current stats and buffs
* @apiSuccess (Object) data.profile Includes name
* @apiSuccess (Object) data.preferences Includes info about appearance and public prefs
* @apiSuccess (Object) data.party Includes basic info about current party and quests
* @apiSuccess (Object) data.items Basic inventory information includes quests, food, potions, eggs, gear, special items
* @apiSuccess (Object) data.achievements Lists current achievements
* @apiSuccess (Object) data.auth Includes latest timestamps
*
* @apiSuccessExample {json} Success-Response:
* {
* "success": true,
* "data": {
* "_id": "99999999-9999-9999-9999-8f14c101aeff",
* "inbox": {
* "optOut": false
* },
* "stats": {
* ---INCLUDES STATS AND BUFFS---
* },
* "profile": {
* "name": "Ezra"
* },
* "preferences": {
* ---INCLUDES INFO ABOUT APPEARANCE AND PUBLIC PREFS---
* },
* "party": {
* "_id": "12345678-0987-abcd-82a6-837c81db4c1e",
* "quest": {
* "RSVPNeeded": false,
* "progress": {}
* },
* },
* "items": {
* "lastDrop": {
* "count": 0,
* "date": "2017-01-15T02:41:35.009Z"
* },
* ----INCLUDES QUESTS, FOOD, POTIONS, EGGS, GEAR, CARDS, SPECIAL ITEMS (E.G. SNOWBALLS)----
* }
* },
* "achievements": {
* "partyUp": true,
* "habitBirthdays": 2,
* },
* "auth": {
* "timestamps": {
* "loggedin": "2017-03-05T12:30:54.545Z",
* "created": "2017-01-12T03:30:11.842Z"
* }
* },
* "id": "99999999-9999-9999-9999-8f14c101aeff"
* }
* }
*)
*
* @apiUse UserNotFound
*/
api.getMember = {
@@ -303,15 +359,28 @@ function _getMembersForItem (type) {
* @apiParam (Query) {Boolean} includeAllPublicFields Query parameter available only when fetching a party. If === `true` then all public fields for members will be returned (like when making a request for a single member)
*
* @apiSuccess {Array} data An array of members, sorted by _id
*
* @apiSuccessExample {json} Success-Response:
* {
* "success": true,
* "data": [
* {
* "_id": "00000001-1111-9999-9000-111111111111",
* "profile": {
* "name": "Jiminy"
* },
* "id": "00000001-1111-9999-9000-111111111111"
* },
* }
*
*
* @apiUse ChallengeNotFound
* @apiUse GroupNotFound
*/
api.getMembersForGroup = {
method: 'GET',
url: '/groups/:groupId/members',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
handler: _getMembersForItem('group-members'),
};
@@ -326,15 +395,28 @@ api.getMembersForGroup = {
*
* @apiSuccess {array} data An array of invites, sorted by _id
*
* @apiSuccessExample {json} Success-Response:
* {
* "success": true,
* "data": [
* {
* "_id": "99f3cb9d-4af8-4ca4-9b82-6b2a6bf59b7a",
* "profile": {
* "name": "DoomSmoocher"
* },
* "id": "99f3cb9d-4af8-4ca4-9b82-6b2a6bf59b7a"
* }
* ]
* }
*
*
* @apiUse ChallengeNotFound
* @apiUse GroupNotFound
*/
api.getInvitesForGroup = {
method: 'GET',
url: '/groups/:groupId/invites',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
handler: _getMembersForItem('group-invites'),
};
@@ -360,9 +442,7 @@ api.getInvitesForGroup = {
api.getMembersForChallenge = {
method: 'GET',
url: '/challenges/:challengeId/members',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
handler: _getMembersForItem('challenge-members'),
};
@@ -376,15 +456,55 @@ api.getMembersForChallenge = {
*
* @apiSuccess {Object} data Return an object with member _id, profile.name and a tasks object with the challenge tasks for the member
*
* @apiSuccessExample {json} Success-Response:
* {
* "data": {
* "_id": "b0413351-405f-416f-8787-947ec1c85199",
* "profile": {"name": "MadPink"},
* "tasks": [
* {
* "_id": "9cd37426-0604-48c3-a950-894a6e72c156",
* "text": "Make sure the place where you sleep is quiet, dark, and cool.",
* "updatedAt": "2017-06-17T17:44:15.916Z",
* "createdAt": "2017-06-17T17:44:15.916Z",
* "reminders": [],
* "group": {
* "approval": {
* "requested": false,
* "approved": false,
* "required": false
* },
* "assignedUsers": []
* },
* "challenge": {
* "taskId": "6d3758b1-071b-4bfa-acd6-755147a7b5f6",
* "id": "4db6bd82-b829-4bf2-bad2-535c14424a3d",
* "shortName": "Take This June 2017"
* },
* "attribute": "str",
* "priority": 1,
* "value": 0,
* "notes": "",
* "type": "todo",
* "checklist": [],
* "collapseChecklist": false,
* "completed": false,
* },
* "startDate": "2016-09-01T05:00:00.000Z",
* "everyX": 1,
* "frequency": "weekly",
* "id": "b207a15e-8bfd-4aa7-9e64-1ba89699da06"
* }
* ]
* }
*
* @apiUse ChallengeNotFound
* @apiUse UserNotFound
*/
api.getChallengeMemberProgress = {
method: 'GET',
url: '/challenges/:challengeId/members/:memberId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
@@ -467,7 +587,7 @@ api.getObjectionsToInteraction = {
* @apiParam (Body) {String} message Body parameter - The message
* @apiParam (Body) {UUID} toUserId Body parameter - The user to contact
*
* @apiSuccess {Object} data An empty Object
* @apiSuccess {Object} data.message The message just sent
*
* @apiUse UserNotFound
*/
@@ -479,25 +599,25 @@ api.sendPrivateMessage = {
req.checkBody('message', res.t('messageRequired')).notEmpty();
req.checkBody('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID();
let validationErrors = req.validationErrors();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let sender = res.locals.user;
let message = req.body.message;
let receiver = await User.findById(req.body.toUserId).exec();
const sender = res.locals.user;
const message = req.body.message;
const receiver = await User.findById(req.body.toUserId).exec();
if (!receiver) throw new NotFound(res.t('userNotFound'));
let objections = sender.getObjectionsToInteraction('send-private-message', receiver);
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
await sender.sendMessage(receiver, { receiverMsg: message });
const messageSent = await sender.sendMessage(receiver, { receiverMsg: message });
if (receiver.preferences.emailNotifications.newPM !== false) {
sendTxnEmail(receiver, 'new-pm', [
{name: 'SENDER', content: getUserInfo(sender, ['name']).name},
]);
}
if (receiver.preferences.pushNotifications.newPM !== false) {
sendPushNotification(
receiver,
@@ -511,7 +631,7 @@ api.sendPrivateMessage = {
);
}
res.respond(200, {});
res.respond(200, {message: messageSent});
},
};
@@ -557,7 +677,7 @@ api.flagPrivateMessage = {
* @apiName TransferGems
* @apiGroup Member
*
* @apiParam (Body) {String} message The message
* @apiParam (Body) {String} message The message to the user
* @apiParam (Body) {UUID} toUserId The toUser _id
* @apiParam (Body) {Integer} gemAmount The number of gems to send
*
@@ -592,6 +712,7 @@ api.transferGems = {
receiver.balance += amount;
sender.balance -= amount;
// @TODO necessary? Also saved when sending the inbox message
let promises = [receiver.save(), sender.save()];
await Promise.all(promises);
+16 -21
View File
@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
let api = {};
// @TODO export this const, cannot export it from here because only routes are exported from controllers
const LAST_ANNOUNCEMENT_TITLE = 'USE CASE SPOTLIGHT AND GUILD SPOTLIGHT ON PARENTING AND FAMILY LIFE, PLUS NEW BEHIND THE SCENES POST!';
const LAST_ANNOUNCEMENT_TITLE = 'FALL FESTIVAL BEGINS! LIMITED EDITION FALL EQUIPMENT, SEASONAL SHOP OPENS, AND NPC OUTFITS!';
const worldDmg = { // @TODO
bailey: false,
};
@@ -26,28 +26,25 @@ api.getNews = {
res.status(200).send({
html: `
<div class="bailey">
<div class="media">
<div class="align-self-center mr-3 ${baileyClass}"></div>
<div class="media align-items-center">
<div class="mr-3 ${baileyClass}"></div>
<div class="media-body">
<h1 class="align-self-center">${res.t('newStuff')}</h1>
<h2>6/22/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
<h2>9/20/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
</div>
</div>
<hr/>
<div class="media align-items-center">
<div class="media-body">
<p>We've got new posts on the blog all about ways to use Habitica to help with parenting and family-related matters! First, there's a <a href='https://habitica.wordpress.com/2018/06/14/guilds-for-parenting-and-family-life/' target='_blank'>Guild Spotlight</a> that highlights the Guilds that can help you as you explore ways to use Habitica to smooth your home and family life. We've also posted a <a href='https://habitica.wordpress.com/2018/06/14/use-case-spotlight-parenting-and-family-life/' target='blank'>Use Case Spotlight</a> featuring a number of great suggestions for using Habitica's task system to manage parenting and family-related tasks! These suggestions were submitted by Habiticans in the <a href='/groups/guild/1d3a10bf-60aa-4806-a38b-82d1084a59e6' target='_blank'>Use Case Spotlights Guild</a>.</p>
</div>
<div class="scene_families ml-3 mb-3"></div>
</div>
<div class="media align-items-center">
<div class="scene_moderators mr-3 mb-3"></div>
<div class="media-body">
<p>Plus, we're collecting user submissions for the next Use Case Spotlight! How do you use Habitica to build a routine to challenge your brain and keep your mind sharp? Well be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog next month, so post your suggestions in the Use Case Spotlight Guild now. We look forward to learning more about how you use Habitica to improve your life and get things done!</p>
<p>There's also a new <a href='https://habitica.wordpress.com/2018/06/21/behind-the-scenes-weird-tales-of-the-mod-slack/' target='_blank'>Behind the Scenes post</a> on the Habitica blog about fun facts and goofy shenanigans from the Moderator Team and their behind-the-scenes chat channel. Check it out!</p>
<div class="small mb-3">by Beffymaroo, the Habitica Staff, and the Moderator Team</div>
</div>
</div>
<div class="promo_fall_festival_2018 center-block"></div>
<h3>Limited Edition Class Outfits!</h3>
<p>From now until October 31st, limited edition outfits are available in the Rewards column! Depending on your class, you can be a Minotaur Warrior, an Alter Ego Rogue, a Carnivorous Plant Healer, or a Candymancer Mage. You'd better get productive to earn enough gold before your time runs out...</p>
<div class="small mb-3">by AnnDeLune, Vikte, QuartzFox, Beffymaroo, and SabreCat</div>
<div class="promo_fall_festival_2017 center-block"></div>
<h3>Seasonal Shop Opens</h3>
<p>The <a href='/shops/seasonal' target='_blank'>Seasonal Shop</a> has opened! It's stocking autumnal Seasonal Edition goodies at the moment, including past fall outfits. Everything there will be available to purchase during the Fall Festival event each year, but it's only open until October 31st, so be sure to stock up now, or you'll have to wait a year to buy these items again!</p>
<div class="small mb-3">by AnnDeLune, ʂʈєƒąʃųƥągųʂ, Katy133, Lilith of Alfheim, Definitely not a villain, ShoGirlGeek. cataclysms, maxpendragon, Lemoness, Beffymaroo, and SabreCat</div>
<h3>NPC Outfits</h3>
<p>Everyone has hastened down to the Flourishing Fields to celebrate this spooky harvest festival. Be sure to check out all the new outfits that people are sporting!</p>
<div class="promo_seasonal_shop center-block"></div>
</div>
`,
});
@@ -65,9 +62,7 @@ api.getNews = {
*/
api.tellMeLaterNews = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/news/tell-me-later',
async handler (req, res) {
const user = res.locals.user;
@@ -23,9 +23,7 @@ let api = {};
api.readNotification = {
method: 'POST',
url: '/notifications/:notificationId/read',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -67,9 +65,7 @@ api.readNotification = {
api.readNotifications = {
method: 'POST',
url: '/notifications/read',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -117,9 +113,7 @@ api.readNotifications = {
api.seeNotification = {
method: 'POST',
url: '/notifications/:notificationId/see',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -168,9 +162,7 @@ api.seeNotification = {
api.seeNotifications = {
method: 'POST',
url: '/notifications/see',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -3,6 +3,7 @@ import {
NotAuthorized,
NotFound,
} from '../../libs/errors';
import { model as PushDevice } from '../../models/pushDevice';
let api = {};
@@ -21,21 +22,19 @@ let api = {};
api.addPushDevice = {
method: 'POST',
url: '/user/push-devices',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
const user = res.locals.user;
req.checkBody('regId', res.t('regIdRequired')).notEmpty();
req.checkBody('type', res.t('typeRequired')).notEmpty().isIn(['ios', 'android']);
let validationErrors = req.validationErrors();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let pushDevices = user.pushDevices;
const pushDevices = user.pushDevices;
let item = {
const item = {
regId: req.body.regId,
type: req.body.type,
};
@@ -44,9 +43,14 @@ api.addPushDevice = {
throw new NotAuthorized(res.t('pushDeviceAlreadyAdded'));
}
pushDevices.push(item);
// Concurrency safe update
const pushDevice = (new PushDevice(item)).toJSON(); // Create a mongo doc
await user.update({
$push: { pushDevices: pushDevice },
}).exec();
await user.save();
// Update the response
user.pushDevices.push(pushDevice);
res.respond(200, user.pushDevices, res.t('pushDeviceAdded'));
},
@@ -66,20 +70,20 @@ api.addPushDevice = {
api.removePushDevice = {
method: 'DELETE',
url: '/user/push-devices/:regId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
const user = res.locals.user;
req.checkParams('regId', res.t('regIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let regId = req.params.regId;
let pushDevices = user.pushDevices;
const regId = req.params.regId;
let indexOfPushDevice = pushDevices.findIndex((element) => {
const pushDevices = user.pushDevices;
const indexOfPushDevice = pushDevices.findIndex((element) => {
return element.regId === regId;
});
@@ -87,8 +91,12 @@ api.removePushDevice = {
throw new NotFound(res.t('pushDeviceNotFound'));
}
// Concurrency safe update
const pullQuery = { $pull: { pushDevices: { $elemMatch: { regId } } } };
await user.update(pullQuery).exec();
// Update the response
pushDevices.splice(indexOfPushDevice, 1);
await user.save();
res.respond(200, user.pushDevices, res.t('pushDeviceRemoved'));
},
+7 -21
View File
@@ -55,9 +55,7 @@ let api = {};
api.inviteToQuest = {
method: 'POST',
url: '/groups/:groupId/quests/invite/:questKey',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let questKey = req.params.questKey;
@@ -171,9 +169,7 @@ api.inviteToQuest = {
api.acceptQuest = {
method: 'POST',
url: '/groups/:groupId/quests/accept',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -232,9 +228,7 @@ api.acceptQuest = {
api.rejectQuest = {
method: 'POST',
url: '/groups/:groupId/quests/reject',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -297,9 +291,7 @@ api.rejectQuest = {
api.forceStart = {
method: 'POST',
url: '/groups/:groupId/quests/force-start',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -357,9 +349,7 @@ api.forceStart = {
api.cancelQuest = {
method: 'POST',
url: '/groups/:groupId/quests/cancel',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
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.
@@ -413,9 +403,7 @@ api.cancelQuest = {
api.abortQuest = {
method: 'POST',
url: '/groups/:groupId/quests/abort',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
// Abort a quest AFTER it has begun (see questCancel for BEFORE)
let user = res.locals.user;
@@ -475,9 +463,7 @@ api.abortQuest = {
api.leaveQuest = {
method: 'POST',
url: '/groups/:groupId/quests/leave',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let groupId = req.params.groupId;
+6 -18
View File
@@ -15,9 +15,7 @@ let api = {};
api.getMarketItems = {
method: 'GET',
url: '/shops/market',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -38,9 +36,7 @@ api.getMarketItems = {
api.getMarketGear = {
method: 'GET',
url: '/shops/market-gear',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -64,9 +60,7 @@ api.getMarketGear = {
api.getQuestShopItems = {
method: 'GET',
url: '/shops/quests',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -88,9 +82,7 @@ api.getQuestShopItems = {
api.getTimeTravelerShopItems = {
method: 'GET',
url: '/shops/time-travelers',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -112,9 +104,7 @@ api.getTimeTravelerShopItems = {
api.getSeasonalShopItems = {
method: 'GET',
url: '/shops/seasonal',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -136,9 +126,7 @@ api.getSeasonalShopItems = {
api.getBackgroundShopItems = {
method: 'GET',
url: '/shops/backgrounds',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
+6 -18
View File
@@ -38,9 +38,7 @@ let api = {};
api.createTag = {
method: 'POST',
url: '/tags',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -66,9 +64,7 @@ api.createTag = {
api.getTags = {
method: 'GET',
url: '/tags',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
res.respond(200, user.tags);
@@ -93,9 +89,7 @@ api.getTags = {
api.getTag = {
method: 'GET',
url: '/tags/:tagId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -132,9 +126,7 @@ api.getTag = {
api.updateTag = {
method: 'PUT',
url: '/tags/:tagId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -176,9 +168,7 @@ api.updateTag = {
api.reorderTags = {
method: 'POST',
url: '/reorder-tags',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -217,9 +207,7 @@ api.reorderTags = {
api.deleteTag = {
method: 'DELETE',
url: '/tags/:tagId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
+35 -57
View File
@@ -5,6 +5,7 @@ import {
} from '../../libs/webhook';
import { removeFromArray } from '../../libs/collectionManipulators';
import * as Tasks from '../../models/task';
import { handleSharedCompletion } from '../../libs/groupTasks';
import { model as Challenge } from '../../models/challenge';
import { model as Group } from '../../models/group';
import { model as User } from '../../models/user';
@@ -157,9 +158,7 @@ let requiredGroupFields = '_id leader tasksOrder name';
api.createUserTasks = {
method: 'POST',
url: '/tasks/user',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let tasks = await createTasks(req, res, {user});
@@ -174,6 +173,7 @@ api.createUserTasks = {
hitType: 'event',
category: 'behavior',
taskType: task.type,
headers: req.headers,
});
}
@@ -230,9 +230,7 @@ api.createUserTasks = {
api.createChallengeTasks = {
method: 'POST',
url: '/tasks/challenge/:challengeId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
@@ -273,7 +271,7 @@ api.createChallengeTasks = {
* @apiGroup Task
*
* @apiParam (Query) {String="habits","dailys","todos","rewards","completedTodos"} type Optional query parameter to return just a type of tasks. By default all types will be returned except completed todos that must be requested separately. The "completedTodos" type returns only the 30 most recently completed.
* @apiParam (Query) [dueDate]
* @apiParam (Query) [dueDate] type Optional date to use for computing the nextDue field for each returned task.
*
* @apiSuccess {Array} data An array of tasks
*
@@ -287,7 +285,7 @@ api.getUserTasks = {
method: 'GET',
url: '/tasks/user',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
userFieldsToInclude: ['_id', 'tasksOrder', 'preferences'],
})],
async handler (req, res) {
let types = Tasks.tasksTypes.map(type => `${type}s`);
@@ -297,10 +295,10 @@ api.getUserTasks = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let user = res.locals.user;
let dueDate = req.query.dueDate;
const user = res.locals.user;
const dueDate = req.query.dueDate;
let tasks = await getTasks(req, res, {user, dueDate});
const tasks = await getTasks(req, res, { user, dueDate });
return res.respond(200, tasks);
},
};
@@ -326,9 +324,7 @@ api.getUserTasks = {
api.getChallengeTasks = {
method: 'GET',
url: '/tasks/challenge/:challengeId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
let types = Tasks.tasksTypes.map(type => `${type}s`);
@@ -378,9 +374,7 @@ api.getChallengeTasks = {
api.getTask = {
method: 'GET',
url: '/tasks/:taskId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let taskId = req.params.taskId;
@@ -434,9 +428,7 @@ api.getTask = {
api.updateTask = {
method: 'PUT',
url: '/tasks/:taskId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let challenge;
@@ -490,6 +482,9 @@ api.updateTask = {
if (sanitizedObj.requiresApproval) {
task.group.approval.required = true;
}
if (sanitizedObj.sharedCompletion) {
task.group.sharedCompletion = sanitizedObj.sharedCompletion;
}
setNextDue(task, user);
let savedTask = await task.save();
@@ -547,9 +542,7 @@ api.updateTask = {
api.scoreTask = {
method: 'POST',
url: '/tasks/:taskId/score/:direction',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('direction', res.t('directionUpDown')).notEmpty().isIn(['up', 'down']);
@@ -598,7 +591,7 @@ api.scoreTask = {
groupId: group._id,
taskId: task._id, // user task id, used to match the notification when the task is approved
userId: user._id,
groupTaskId: task.group.id, // the original task id
groupTaskId: task.group.taskId, // the original task id
direction,
});
managerPromises.push(manager.save());
@@ -653,6 +646,12 @@ api.scoreTask = {
user.save(),
task.save(),
];
if (task.group && task.group.taskId) {
await handleSharedCompletion(task);
}
// Save results and handle request
if (taskOrderPromise) promises.push(taskOrderPromise);
let results = await Promise.all(promises);
@@ -692,6 +691,7 @@ api.scoreTask = {
category: 'behavior',
taskType: task.type,
direction,
headers: req.headers,
});
}
},
@@ -716,9 +716,7 @@ api.scoreTask = {
api.moveTask = {
method: 'POST',
url: '/tasks/:taskId/move/to/:position',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
@@ -787,9 +785,7 @@ api.moveTask = {
api.addChecklistItem = {
method: 'POST',
url: '/tasks/:taskId/checklist',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let challenge;
@@ -849,9 +845,7 @@ api.addChecklistItem = {
api.scoreCheckListItem = {
method: 'POST',
url: '/tasks/:taskId/checklist/:itemId/score',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -905,9 +899,7 @@ api.scoreCheckListItem = {
api.updateChecklistItem = {
method: 'PUT',
url: '/tasks/:taskId/checklist/:itemId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let challenge;
@@ -972,9 +964,7 @@ api.updateChecklistItem = {
api.removeChecklistItem = {
method: 'DELETE',
url: '/tasks/:taskId/checklist/:itemId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let challenge;
@@ -1037,9 +1027,7 @@ api.removeChecklistItem = {
api.addTagToTask = {
method: 'POST',
url: '/tasks/:taskId/tags/:tagId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -1088,9 +1076,7 @@ api.addTagToTask = {
api.removeTagFromTask = {
method: 'DELETE',
url: '/tasks/:taskId/tags/:tagId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -1135,9 +1121,7 @@ api.removeTagFromTask = {
api.unlinkAllTasks = {
method: 'POST',
url: '/tasks/unlink-all/:challengeId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
req.checkQuery('keep', apiError('keepOrRemoveAll')).notEmpty().isIn(['keep-all', 'remove-all']);
@@ -1204,9 +1188,7 @@ api.unlinkAllTasks = {
api.unlinkOneTask = {
method: 'POST',
url: '/tasks/unlink-one/:taskId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
req.checkQuery('keep', apiError('keepOrRemove')).notEmpty().isIn(['keep', 'remove']);
@@ -1256,9 +1238,7 @@ api.unlinkOneTask = {
api.clearCompletedTodos = {
method: 'POST',
url: '/tasks/clearCompletedTodos',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -1309,9 +1289,7 @@ api.clearCompletedTodos = {
api.deleteTask = {
method: 'DELETE',
url: '/tasks/:taskId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let challenge;
@@ -12,10 +12,13 @@ import {
getTasks,
moveTask,
} from '../../../libs/taskManager';
import { handleSharedCompletion } from '../../../libs/groupTasks';
import apiError from '../../../libs/apiError';
let requiredGroupFields = '_id leader tasksOrder name';
// @TODO: abstract to task lib
let types = Tasks.tasksTypes.map(type => `${type}s`);
types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future
function canNotEditTasks (group, user, assignedUserId) {
let isNotGroupLeader = group.leader !== user._id;
@@ -39,9 +42,7 @@ let api = {};
api.createGroupTasks = {
method: 'POST',
url: '/tasks/group/:groupId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
@@ -85,9 +86,7 @@ api.createGroupTasks = {
api.getGroupTasks = {
method: 'GET',
url: '/tasks/group/:groupId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);
@@ -120,9 +119,7 @@ api.getGroupTasks = {
api.groupMoveTask = {
method: 'POST',
url: '/group-tasks/:taskId/move/to/:position',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
@@ -173,9 +170,7 @@ api.groupMoveTask = {
api.assignTask = {
method: 'POST',
url: '/tasks/:taskId/assign/:assignedUserId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
@@ -235,9 +230,7 @@ api.assignTask = {
api.unassignTask = {
method: 'POST',
url: '/tasks/:taskId/unassign/:assignedUserId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
@@ -287,9 +280,7 @@ api.unassignTask = {
api.approveTask = {
method: 'POST',
url: '/tasks/:taskId/approve/:userId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
@@ -338,7 +329,7 @@ api.approveTask = {
}
// Remove old notifications
let managerPromises = [];
let approvalPromises = [];
managers.forEach((manager) => {
let notificationIndex = manager.notifications.findIndex(function findNotification (notification) {
return notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL';
@@ -346,7 +337,7 @@ api.approveTask = {
if (notificationIndex !== -1) {
manager.notifications.splice(notificationIndex, 1);
managerPromises.push(manager.save());
approvalPromises.push(manager.save());
}
});
@@ -362,9 +353,11 @@ api.approveTask = {
direction,
});
managerPromises.push(task.save());
managerPromises.push(assignedUser.save());
await Promise.all(managerPromises);
await handleSharedCompletion(task);
approvalPromises.push(task.save());
approvalPromises.push(assignedUser.save());
await Promise.all(approvalPromises);
res.respond(200, task);
},
@@ -385,9 +378,7 @@ api.approveTask = {
api.taskNeedsWork = {
method: 'POST',
url: '/tasks/:taskId/needs-work/:userId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
@@ -484,9 +475,7 @@ api.taskNeedsWork = {
api.getGroupApprovals = {
method: 'GET',
url: '/approvals/group/:groupId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
+66 -307
View File
@@ -8,9 +8,6 @@ import {
basicFields as basicGroupFields,
model as Group,
} from '../../models/group';
import {
model as User,
} from '../../models/user';
import * as Tasks from '../../models/task';
import _ from 'lodash';
import * as passwordUtils from '../../libs/password';
@@ -22,6 +19,8 @@ import {
sendTxn as txnEmail,
} from '../../libs/email';
import Queue from '../../libs/queue';
import * as inboxLib from '../../libs/inbox';
import * as userLib from '../../libs/user';
import nconf from 'nconf';
import get from 'lodash/get';
@@ -35,6 +34,8 @@ const DELETE_CONFIRMATION = 'DELETE';
let api = {};
/* NOTE this route has also an API v4 version */
/**
* @api {get} /api/v3/user Get the authenticated user's profile
* @apiName UserGet
@@ -47,7 +48,7 @@ let api = {};
* Flags (including armoire, tutorial, tour etc...)
* Guilds
* History (including timestamps and values)
* Inbox (includes message history)
* Inbox
* Invitations (to parties/guilds)
* Items (character's full inventory)
* New Messages (flags for groups/guilds that have new messages)
@@ -83,20 +84,7 @@ api.getUser = {
middlewares: [authWithHeaders()],
url: '/user',
async handler (req, res) {
let user = res.locals.user;
let userToJSON = user.toJSON();
// Remove apiToken from response TODO make it private at the user level? returned in signup/login
delete userToJSON.apiToken;
if (!req.query.userFields) {
let {daysMissed} = user.daysUserHasMissed(new Date(), req);
userToJSON.needsCron = false;
if (daysMissed > 0) userToJSON.needsCron = true;
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
}
return res.respond(200, userToJSON);
await userLib.get(req, res, { isV3: true });
},
};
@@ -128,9 +116,7 @@ api.getUser = {
*/
api.getBuyList = {
method: 'GET',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/inventory/buy',
async handler (req, res) {
let list = _.cloneDeep(common.updateStore(res.locals.user));
@@ -147,7 +133,7 @@ api.getBuyList = {
};
/**
* @api {get} /api/v3/user/in-app-rewards Get the in app items appaearing in the user's reward column
* @api {get} /api/v3/user/in-app-rewards Get the in app items appearing in the user's reward column
* @apiName UserGetInAppRewards
* @apiGroup User
*
@@ -173,9 +159,7 @@ api.getBuyList = {
*/
api.getInAppRewardsList = {
method: 'GET',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/in-app-rewards',
async handler (req, res) {
let list = common.inAppRewards(res.locals.user);
@@ -191,78 +175,7 @@ api.getInAppRewardsList = {
},
};
let updatablePaths = [
'_ABtests.counter',
'flags.customizationsNotification',
'flags.showTour',
'flags.tour',
'flags.tutorial',
'flags.communityGuidelinesAccepted',
'flags.welcomed',
'flags.cardReceived',
'flags.warnedLowHealth',
'flags.newStuff',
'achievements',
'party.order',
'party.orderAscending',
'party.quest.completed',
'party.quest.RSVPNeeded',
'preferences',
'profile',
'stats',
'inbox.optOut',
'tags',
];
// This tells us for which paths users can call `PUT /user`.
// The trick here is to only accept leaf paths, not root/intermediate paths (see http://goo.gl/OEzkAs)
let acceptablePUTPaths = _.reduce(require('./../../models/user').schema.paths, (accumulator, val, leaf) => {
let found = _.find(updatablePaths, (rootPath) => {
return leaf.indexOf(rootPath) === 0;
});
if (found) accumulator[leaf] = true;
return accumulator;
}, {});
let restrictedPUTSubPaths = [
'stats.class',
'preferences.disableClasses',
'preferences.sleep',
'preferences.webhooks',
];
_.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 appearance = _.get(common.content.appearances, itemPath);
if (!appearance) return false;
if (appearance.price === 0) return true;
return _.get(user.purchased, itemPath);
};
/* NOTE this route has also an API v4 version */
/**
* @api {put} /api/v3/user Update the user
@@ -297,67 +210,7 @@ api.updateUser = {
middlewares: [authWithHeaders()],
url: '/user',
async handler (req, res) {
let user = res.locals.user;
let promisesForTagsRemoval = [];
_.each(req.body, (val, key) => {
let purchasable = requiresPurchase[key];
if (purchasable && !checkPreferencePurchase(user, purchasable, val)) {
throw new NotAuthorized(res.t('mustPurchaseToSet', { val, key }));
}
if (acceptablePUTPaths[key] && key !== 'tags') {
_.set(user, key, val);
} else if (key === 'tags') {
if (!Array.isArray(val)) throw new BadRequest('mustBeArray');
const removedTagsIds = [];
const oldTags = [];
// Keep challenge and group tags
user.tags.forEach(t => {
if (t.group) {
oldTags.push(t);
} else {
removedTagsIds.push(t.id);
}
});
user.tags = oldTags;
val.forEach(t => {
let oldI = removedTagsIds.findIndex(id => id === t.id);
if (oldI > -1) {
removedTagsIds.splice(oldI, 1);
}
user.tags.push(t);
});
// Remove from all the tasks
// NOTE each tag to remove requires a query
promisesForTagsRemoval = removedTagsIds.map(tagId => {
return Tasks.Task.update({
userId: user._id,
}, {
$pull: {
tags: tagId,
},
}, {multi: true}).exec();
});
} else {
throw new NotAuthorized(res.t('messageUserOperationProtected', { operation: key }));
}
});
await Promise.all([user.save()].concat(promisesForTagsRemoval));
return res.respond(200, user);
await userLib.update(req, res, { isV3: true });
},
};
@@ -488,7 +341,7 @@ api.getUserAnonymized = {
middlewares: [authWithHeaders()],
url: '/user/anonymized',
async handler (req, res) {
let user = res.locals.user.toJSON();
let user = await res.locals.user.toJSONWithInbox();
user.stats.toNextLevel = common.tnl(user.stats.lvl);
user.stats.maxHealth = common.maxHealth;
user.stats.maxMP = common.statsComputed(res.locals.user).maxMP;
@@ -513,6 +366,7 @@ api.getUserAnonymized = {
_.forEach(user.inbox.messages, (msg) => {
msg.text = 'inbox message text';
});
_.forEach(user.tags, (tag) => {
tag.name = 'tag';
tag.challenge = 'challenge';
@@ -556,9 +410,7 @@ api.getUserAnonymized = {
*/
api.sleep = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/sleep',
async handler (req, res) {
let user = res.locals.user;
@@ -602,9 +454,7 @@ const buyKnownKeys = ['armoire', 'mystery', 'potion', 'quest', 'special'];
*/
api.buy = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/buy/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -668,9 +518,7 @@ api.buy = {
*/
api.buyGear = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/buy-gear/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -710,9 +558,7 @@ api.buyGear = {
*/
api.buyArmoire = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/buy-armoire',
async handler (req, res) {
let user = res.locals.user;
@@ -752,9 +598,7 @@ api.buyArmoire = {
*/
api.buyHealthPotion = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/buy-health-potion',
async handler (req, res) {
let user = res.locals.user;
@@ -796,9 +640,7 @@ api.buyHealthPotion = {
*/
api.buyMysterySet = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/buy-mystery-set/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -841,9 +683,7 @@ api.buyMysterySet = {
*/
api.buyQuest = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/buy-quest/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -883,9 +723,7 @@ api.buyQuest = {
*/
api.buySpecialSpell = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/buy-special-spell/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -929,9 +767,7 @@ api.buySpecialSpell = {
*/
api.hatch = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/hatch/:egg/:hatchingPotion',
async handler (req, res) {
let user = res.locals.user;
@@ -983,9 +819,7 @@ api.hatch = {
*/
api.equip = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/equip/:type/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -1020,9 +854,7 @@ api.equip = {
*/
api.feed = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/feed/:pet/:food',
async handler (req, res) {
let user = res.locals.user;
@@ -1066,9 +898,7 @@ api.feed = {
*/
api.changeClass = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/change-class',
async handler (req, res) {
let user = res.locals.user;
@@ -1089,9 +919,7 @@ api.changeClass = {
*/
api.disableClasses = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/disable-classes',
async handler (req, res) {
let user = res.locals.user;
@@ -1123,9 +951,7 @@ api.disableClasses = {
*/
api.purchase = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/purchase/:type/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -1172,9 +998,7 @@ api.purchase = {
*/
api.userPurchaseHourglass = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/purchase-hourglass/:type/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -1226,9 +1050,7 @@ api.userPurchaseHourglass = {
*/
api.readCard = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/read-card/:cardType',
async handler (req, res) {
let user = res.locals.user;
@@ -1270,9 +1092,7 @@ api.readCard = {
*/
api.userOpenMysteryItem = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/open-mystery-item',
async handler (req, res) {
let user = res.locals.user;
@@ -1304,9 +1124,7 @@ api.userOpenMysteryItem = {
*/
api.userReleasePets = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/release-pets',
async handler (req, res) {
let user = res.locals.user;
@@ -1355,9 +1173,7 @@ api.userReleasePets = {
*/
api.userReleaseBoth = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/release-both',
async handler (req, res) {
let user = res.locals.user;
@@ -1393,9 +1209,7 @@ api.userReleaseBoth = {
*/
api.userReleaseMounts = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/release-mounts',
async handler (req, res) {
let user = res.locals.user;
@@ -1425,9 +1239,7 @@ api.userReleaseMounts = {
*/
api.userSell = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/sell/:type/:key',
async handler (req, res) {
let user = res.locals.user;
@@ -1445,8 +1257,8 @@ api.userSell = {
* @apiParam (Query) {String} path Full path to unlock. See "content" API call for list of items.
*
* @apiParamExample {curl}
* curl -x POST http://habitica.com/api/v3/user/unlock?path=background.midnight_clouds
* curl -x POST http://habitica.com/api/v3/user/unlock?path=hair.color.midnight
* curl -X POST http://habitica.com/api/v3/user/unlock?path=background.midnight_clouds
* curl -X POST http://habitica.com/api/v3/user/unlock?path=hair.color.midnight
*
* @apiSuccess {Object} data.purchased
* @apiSuccess {Object} data.items
@@ -1470,9 +1282,7 @@ api.userSell = {
*/
api.userUnlock = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/unlock',
async handler (req, res) {
let user = res.locals.user;
@@ -1498,9 +1308,7 @@ api.userUnlock = {
*/
api.userRevive = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/revive',
async handler (req, res) {
let user = res.locals.user;
@@ -1510,6 +1318,8 @@ api.userRevive = {
},
};
/* NOTE this route has also an API v4 version */
/**
* @api {post} /api/v3/user/rebirth Use Orb of Rebirth on user
* @apiName UserRebirth
@@ -1540,27 +1350,10 @@ api.userRevive = {
*/
api.userRebirth = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/rebirth',
async handler (req, res) {
let user = res.locals.user;
let tasks = await Tasks.Task.find({
userId: user._id,
type: {$in: ['daily', 'habit', 'todo']},
...Tasks.taskIsGroupOrChallengeQuery,
}).exec();
let rebirthRes = common.ops.rebirth(user, tasks, req, res.analytics);
let toSave = tasks.map(task => task.save());
toSave.push(user.save());
await Promise.all(toSave);
res.respond(200, ...rebirthRes);
await userLib.rebirth(req, res, { isV3: true });
},
};
@@ -1591,6 +1384,8 @@ api.blockUser = {
},
};
/* NOTE this route has also an API v4 version */
/**
* @api {delete} /api/v3/user/messages/:id Delete a message
* @apiName deleteMessage
@@ -1625,12 +1420,15 @@ api.deleteMessage = {
url: '/user/messages/:id',
async handler (req, res) {
let user = res.locals.user;
let deletePMRes = common.ops.deletePM(user, req);
await user.save();
res.respond(200, ...deletePMRes);
await inboxLib.deleteMessage(user, req.params.id);
res.respond(200, ...[await inboxLib.getUserInbox(user, false)]);
},
};
/* NOTE this route has also an API v4 version */
/**
* @api {delete} /api/v3/user/messages Delete all messages
* @apiName clearMessages
@@ -1647,9 +1445,10 @@ api.clearMessages = {
url: '/user/messages',
async handler (req, res) {
let user = res.locals.user;
let clearPMsRes = common.ops.clearPMs(user, req);
await user.save();
res.respond(200, ...clearPMsRes);
await inboxLib.clearPMs(user);
res.respond(200, ...[]);
},
};
@@ -1658,7 +1457,7 @@ api.clearMessages = {
* @apiName markPmsRead
* @apiGroup User
*
* @apiSuccess {Object} data user.inbox.messages
* @apiSuccess {Object} data user.inbox.newMessages
*
* @apiSuccessExample {json}
* {"success":true,"data":[0,"Your private messages have been marked as read"],"notifications":[]}
@@ -1676,6 +1475,8 @@ api.markPmsRead = {
},
};
/* NOTE this route has also an API v4 version */
/**
* @api {post} /api/v3/user/reroll Reroll a user using the Fortify Potion
* @apiName UserReroll
@@ -1700,29 +1501,15 @@ api.markPmsRead = {
*/
api.userReroll = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/reroll',
async handler (req, res) {
let user = res.locals.user;
let query = {
userId: user._id,
type: {$in: ['daily', 'habit', 'todo']},
...Tasks.taskIsGroupOrChallengeQuery,
};
let tasks = await Tasks.Task.find(query).exec();
let rerollRes = common.ops.reroll(user, tasks, req, res.analytics);
let promises = tasks.map(task => task.save());
promises.push(user.save());
await Promise.all(promises);
res.respond(200, ...rerollRes);
await userLib.reroll(req, res, { isV3: true });
},
};
/* NOTE this route has also an API v4 version */
/**
* @api {post} /api/v3/user/reset Reset user
* @apiName UserReset
@@ -1746,32 +1533,10 @@ api.userReroll = {
*/
api.userReset = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/reset',
async handler (req, res) {
let user = res.locals.user;
let tasks = await Tasks.Task.find({
userId: user._id,
...Tasks.taskIsGroupOrChallengeQuery,
}).select('_id type challenge group').exec();
let resetRes = common.ops.reset(user, tasks);
await Promise.all([
Tasks.Task.remove({_id: {$in: resetRes[0].tasksToRemove}, userId: user._id}),
user.save(),
]);
res.analytics.track('account reset', {
uuid: user._id,
hitType: 'event',
category: 'behavior',
});
res.respond(200, ...resetRes);
await userLib.reset(req, res, { isV3: true });
},
};
@@ -1799,9 +1564,7 @@ api.userReset = {
*/
api.setCustomDayStart = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/custom-day-start',
async handler (req, res) {
let user = res.locals.user;
@@ -1839,9 +1602,7 @@ api.setCustomDayStart = {
*/
api.togglePinnedItem = {
method: 'GET',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/toggle-pinned-item/:type/:path',
async handler (req, res) {
let user = res.locals.user;
@@ -1879,9 +1640,7 @@ api.togglePinnedItem = {
api.movePinnedItem = {
method: 'POST',
url: '/user/move-pinned-item/:path/move/to/:position',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('path', res.t('taskIdRequired')).notEmpty();
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
@@ -1,25 +1,12 @@
import { authWithHeaders } from '../../../middlewares/auth';
import common from '../../../../common';
import {
model as Group,
} from '../../../models/group';
import {
NotAuthorized,
NotFound,
} from '../../../libs/errors';
import {
castTaskSpell,
castMultiTaskSpell,
castSelfSpell,
castPartySpell,
castUserSpell,
castSpell,
} from '../../../libs/spells';
import apiError from '../../../libs/apiError';
const partyMembersFields = 'profile.name stats achievements items.special';
let api = {};
/* NOTE this route has also an API v4 version */
/**
* @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target
* @apiName UserCast
@@ -72,69 +59,9 @@ api.castSpell = {
middlewares: [authWithHeaders()],
url: '/user/class/cast/:spellId',
async handler (req, res) {
let user = res.locals.user;
let spellId = req.params.spellId;
let targetId = req.query.targetId;
const quantity = req.body.quantity || 1;
// optional because not required by all targetTypes, presence is checked later if necessary
req.checkQuery('targetId', res.t('targetIdUUID')).optional().isUUID();
let reqValidationErrors = req.validationErrors();
if (reqValidationErrors) throw reqValidationErrors;
let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class;
let spell = common.content.spells[klass][spellId];
if (!spell) throw new NotFound(apiError('spellNotFound', {spellId}));
if (spell.mana > user.stats.mp) throw new NotAuthorized(res.t('notEnoughMana'));
if (spell.value > user.stats.gp && !spell.previousPurchase) throw new NotAuthorized(res.t('messageNotEnoughGold'));
if (spell.lvl > user.stats.lvl) throw new NotAuthorized(res.t('spellLevelTooHigh', {level: spell.lvl}));
let targetType = spell.target;
if (targetType === 'task') {
const results = await castTaskSpell(res, req, targetId, user, spell, quantity);
res.respond(200, {
user: results[0],
task: results[1],
});
} else if (targetType === 'self') {
await castSelfSpell(req, user, spell, quantity);
res.respond(200, { user });
} else if (targetType === 'tasks') { // new target type in v3: when all the user's tasks are necessary
const response = await castMultiTaskSpell(req, user, spell, quantity);
res.respond(200, response);
} else if (targetType === 'party' || targetType === 'user') {
const party = await Group.getGroup({groupId: 'party', user});
// arrays of users when targetType is 'party' otherwise single users
let partyMembers;
if (targetType === 'party') {
partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity);
} else {
partyMembers = await castUserSpell(res, req, party, partyMembers, targetId, user, spell, quantity);
}
let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers];
// Only return some fields.
// See comment above on why we can't just select the necessary fields when querying
partyMembersRes = partyMembersRes.map(partyMember => {
return common.pickDeep(partyMember.toJSON(), common.$w(partyMembersFields));
});
res.respond(200, {
partyMembers: partyMembersRes,
user,
});
if (party && !spell.silent) {
let message = `\`${user.profile.name} casts ${spell.text()}${targetType === 'user' ? ` on ${partyMembers.profile.name}` : ' for the party'}.\``;
const newChatMessage = party.sendChat(message);
await newChatMessage.save();
}
}
await castSpell(req, res, {
isV3: true,
});
},
};
+22 -27
View File
@@ -5,31 +5,29 @@ import { authWithHeaders } from '../../../middlewares/auth';
let api = {};
/**
* @api {post} /api/v3/user/allocate Allocate a single attribute point
* @api {post} /api/v3/user/allocate Allocate a single Stat Point (previously called Attribute Point)
* @apiName UserAllocate
* @apiGroup User
*
* @apiParam (Body) {String="str","con","int","per"} stat Query parameter - Default ='str'
* @apiParam (Query) {String="str","con","int","per"} stat The Stat to increase. Default is 'str'
*
* @apiParamExample {json} Example request
* {"stat":"int"}
* @apiParamExample {curl}
* curl -X POST -d "" https://habitica.com/api/v3/user/allocate?stat=int
*
* @apiSuccess {Object} data Returns stats from the user profile
* @apiSuccess {Object} data Returns stats and notifications from the user profile
*
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
* @apiError {NotAuthorized} NoPoints You don't have enough Stat Points.
*
* @apiErrorExample {json}
* {
* "success": false,
* "error": "NotAuthorized",
* "message": "You don't have enough attribute points."
* "message": "You don't have enough Stat Points."
* }
*/
api.allocate = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/allocate',
async handler (req, res) {
let user = res.locals.user;
@@ -40,7 +38,7 @@ api.allocate = {
};
/**
* @api {post} /api/v3/user/allocate-bulk Allocate multiple attribute points
* @api {post} /api/v3/user/allocate-bulk Allocate multiple Stat Points
* @apiName UserAllocateBulk
* @apiGroup User
*
@@ -49,29 +47,27 @@ api.allocate = {
* @apiParamExample {json} Example request
* {
* stats: {
* 'int': int,
* 'str': int,
* 'con': int,
* 'per': int,
* },
* "int": int,
* "str": str,
* "con": con,
* "per": per
* }
* }
*
* @apiSuccess {Object} data Returns stats from the user profile
* @apiSuccess {Object} data Returns stats and notifications from the user profile
*
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
* @apiError {NotAuthorized} NoPoints You don't have enough Stat Points.
*
* @apiErrorExample {json}
* {
* "success": false,
* "error": "NotAuthorized",
* "message": "You don't have enough attribute points."
* "message": "You don't have enough Stat Points."
* }
*/
api.allocateBulk = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/allocate-bulk',
async handler (req, res) {
let user = res.locals.user;
@@ -82,7 +78,7 @@ api.allocateBulk = {
};
/**
* @api {post} /api/v3/user/allocate-now Allocate all attribute points
* @api {post} /api/v3/user/allocate-now Allocate all Stat Points
* @apiDescription Uses the user's chosen automatic allocation method, or if none, assigns all to STR. Note: will return success, even if there are 0 points to allocate.
* @apiName UserAllocateNow
* @apiGroup User
@@ -119,7 +115,8 @@ api.allocateBulk = {
* "per": 0,
* "str": 0,
* "con": 0
* }
* },
* "notifications": [ .... ],
* }
* }
*
@@ -127,9 +124,7 @@ api.allocateBulk = {
*/
api.allocateNow = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/allocate-now',
async handler (req, res) {
let user = res.locals.user;
+3 -9
View File
@@ -73,9 +73,7 @@ let api = {};
*/
api.addWebhook = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/webhook',
async handler (req, res) {
let user = res.locals.user;
@@ -135,9 +133,7 @@ api.addWebhook = {
*/
api.updateWebhook = {
method: 'PUT',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/webhook/:id',
async handler (req, res) {
let user = res.locals.user;
@@ -188,9 +184,7 @@ api.updateWebhook = {
*/
api.deleteWebhook = {
method: 'DELETE',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/webhook/:id',
async handler (req, res) {
let user = res.locals.user;