Merge branch 'develop' into party-chat-translations

# Conflicts:
#	test/api/unit/models/group.test.js
#	website/server/controllers/api-v3/user/spells.js
#	website/server/models/chat.js
#	website/server/models/group.js
This commit is contained in:
Mateus Etto
2018-10-08 18:44:02 +09:00
645 changed files with 44839 additions and 37081 deletions

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,115 +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({
username: {
notEmpty: true,
errorMessage: res.t('missingUsername'),
// TODO use the constants in the error message above
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')},
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')},
},
email: {
notEmpty: true,
errorMessage: res.t('missingEmail'),
isEmail: {errorMessage: res.t('notAnEmail')},
},
password: {
notEmpty: true,
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 });
},
};
@@ -398,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;
@@ -468,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;
@@ -524,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;
@@ -636,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;
@@ -725,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;

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';
@@ -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;
@@ -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();
@@ -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;

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,
@@ -62,9 +62,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;
@@ -103,9 +101,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;
@@ -227,9 +223,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;
@@ -286,9 +280,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();
@@ -317,9 +309,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;
@@ -389,9 +379,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;
@@ -457,9 +445,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;

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();

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));
@@ -293,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;
@@ -443,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;
@@ -508,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;
@@ -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();
@@ -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;
@@ -1176,7 +1160,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
api.inviteToGroup = {
method: 'POST',
url: '/groups/:groupId/invite',
middlewares: [authWithHeaders({})],
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
@@ -1249,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;
@@ -1300,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;
@@ -1355,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;

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({

View File

@@ -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;

View File

@@ -32,6 +32,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 = {
@@ -302,15 +358,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'),
};
@@ -325,15 +394,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'),
};
@@ -359,9 +441,7 @@ api.getInvitesForGroup = {
api.getMembersForChallenge = {
method: 'GET',
url: '/challenges/:challengeId/members',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
handler: _getMembersForItem('challenge-members'),
};
@@ -375,15 +455,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();
@@ -466,7 +586,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
*/
@@ -489,7 +609,7 @@ api.sendPrivateMessage = {
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
const newMessage = await sender.sendMessage(receiver, { receiverMsg: message });
const messageSent = await sender.sendMessage(receiver, { receiverMsg: message });
if (receiver.preferences.emailNotifications.newPM !== false) {
sendTxnEmail(receiver, 'new-pm', [
@@ -510,7 +630,7 @@ api.sendPrivateMessage = {
);
}
res.respond(200, { message: newMessage });
res.respond(200, {message: messageSent});
},
};
@@ -519,7 +639,7 @@ api.sendPrivateMessage = {
* @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
*
@@ -554,6 +674,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);

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 = 'AUGUST SUBSCRIBER ITEMS AND WIKI SPOTLIGHT ON CUSTOMIZING THE HABITICA EXPERIENCE';
const LAST_ANNOUNCEMENT_TITLE = 'SLEEPY AVATARS; LAST CHANCE FOR AUTUMNAL ARMOR SET AND FOREST FRIENDS BUNDLE';
const worldDmg = { // @TODO
bailey: false,
};
@@ -30,27 +30,24 @@ api.getNews = {
<div class="mr-3 ${baileyClass}"></div>
<div class="media-body">
<h1 class="align-self-center">${res.t('newStuff')}</h1>
<h2>8/23/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
<h2>9/27/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
</div>
</div>
<hr/>
<div class="media align-items-center">
<div class="media-body">
<h3>August Subscriber Set Revealed!</h3>
<p>Subscriber Items for August have been revealed: the Lava Dragon Item Set! You only have until August 31 to receive the item set when you <a href='/user/settings/subscription' target='_blank'>subscribe</a>. If you're already an active subscriber, reload the site and then head to Inventory > Items to claim your gear!</p>
</div>
<div class="promo_mystery_201808 ml-3"></div>
</div>
<p>Subscribers also receive the ability to buy Gems for Gold -- the longer you subscribe, the more Gems you can buy per month! There are other perks as well, such as longer access to uncompressed data and a cute Jackalope pet. Best of all, subscriptions let us keep Habitica running. Thank you very much for your support -- it means a lot to us.</p>
<h3>Is Your Avatar Asleep?</h3>
<p>Hey Habiticans! You may have noticed we experienced a server outage on 25-September. During this issue, we <a href='https://habitica.wikia.com/wiki/Rest_in_the_Inn' target='_blank'>put all users in the Inn</a> to prevent any unfair damage. To check out of the Inn and resume damage from Dailies as well as boss damage in Quests, go to Menu>Social>Tavern>Details (on mobile) or Guilds>Tavern (on web) and tap the orange banner that says "Resume Damage."</p>
<p>Thank you all for your patience and support during the outage. We are always grateful for our exceptionally wonderful community! <3</p>
<div class="promo_mystery_201809 center-block"></div>
<h3>Last Chance for Autumnal Armor Set</h3>
<p>Reminder: this weekend is your last chance to <a href='/user/settings/subscription' target='_blank'>subscribe</a> and receive the Autumnal Set! Subscribing also lets you buy Gems for Gold. The longer your subscription, the more Gems you get!</p>
<p>Thanks so much for your support! You help keep Habitica running.</p>
<div class="small mb-3">by Beffymaroo</div>
<div class="media align-items-center">
<div class="scene_casting_spells mr-3 mb-3"></div>
<div class="media-body">
<h3>Blog Post: Creating a Unique Experience</h3>
<p>This month's <a href='https://habitica.wordpress.com/2018/08/22/creating-a-unique-experience/' target='_blank'>featured Wiki article</a> is about using Habitica's features to create a unique experience! We hope that it will help you as you customize Habitica to make the app even more motivating and fun. Be sure to check it out, and let us know what you think by reaching out on <a href='https://twitter.com/habitica' target='_blank'>Twitter</a>, <a href='http://blog.habitrpg.com' target='_blank'>Tumblr</a>, and <a href='https://facebook.com/habitica' target='_blank'>Facebook</a>.</p>
<div class="small mb-3">by shanaqui and the Wiki Wizards</div>
</div>
</div>
<div class="promo_forest_friends_bundle center-block"></div>
<h3>Last Chance for Forest Friends Quest Bundle</h3>
<p>This is also the final week to buy the discounted Forest Friends Pet Quest Bundle, featuring the Deer, Hedgehog, and Treeling quests all for seven Gems! Be sure to grab this bundle from the <a href='/shops/quests' target='_blank'>Quest Shop</a> before it scampers into the underbrush!</p>
<div class="small">by Beffymaroo and SabreCat</div>
<div class="small">Art by Uncommon Criminal, InspectorCaracal, Leephon, aurakami, FuzzyTrees, PainterProphet, and plumilla</div>
<div class="small mb-3">Writing by Daniel the Bard, Flutter Bee, and Lemoness</div>
</div>
`,
});
@@ -68,9 +65,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;

View File

@@ -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;

View File

@@ -22,9 +22,7 @@ let api = {};
api.addPushDevice = {
method: 'POST',
url: '/user/push-devices',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
const user = res.locals.user;
@@ -72,9 +70,7 @@ api.addPushDevice = {
api.removePushDevice = {
method: 'DELETE',
url: '/user/push-devices/:regId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
async handler (req, res) {
const user = res.locals.user;

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;
@@ -482,9 +470,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;

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;

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;

View File

@@ -158,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});
@@ -232,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();
@@ -328,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`);
@@ -380,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;
@@ -436,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;
@@ -552,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']);
@@ -728,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();
@@ -799,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;
@@ -861,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;
@@ -917,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;
@@ -984,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;
@@ -1049,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;
@@ -1100,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;
@@ -1147,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']);
@@ -1216,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']);
@@ -1268,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;
@@ -1321,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;

View File

@@ -42,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();
@@ -88,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);
@@ -123,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();
@@ -176,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();
@@ -245,9 +237,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();
@@ -297,9 +287,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();
@@ -397,9 +385,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();
@@ -496,9 +482,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();

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));
@@ -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;
@@ -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();

View File

@@ -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,90 +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) {
if (targetType === 'user') {
const newChatMessage = party.sendChat({
message: `\`${common.i18n.t('chatCastSpellUser', {username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name}, 'en')}\``,
info: {
type: 'spell_cast_user',
user: user.profile.name,
class: klass,
spell: spellId,
target: partyMembers.profile.name,
},
});
await newChatMessage.save();
} else {
const newChatMessage = party.sendChat({
message: `\`${common.i18n.t('chatCastSpellParty', {username: user.profile.name, spell: spell.text()}, 'en')}\``,
info: {
type: 'spell_cast_party',
user: user.profile.name,
class: klass,
spell: spellId,
},
});
await newChatMessage.save();
}
}
}
await castSpell(req, res, {
isV3: true,
});
},
};

View File

@@ -27,9 +27,7 @@ let api = {};
*/
api.allocate = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/allocate',
async handler (req, res) {
let user = res.locals.user;
@@ -69,9 +67,7 @@ api.allocate = {
*/
api.allocateBulk = {
method: 'POST',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
middlewares: [authWithHeaders()],
url: '/user/allocate-bulk',
async handler (req, res) {
let user = res.locals.user;
@@ -128,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;

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;