mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 07:07:35 +01:00
Move inbox to its own model (#10428)
* shared model for chat and inbox * disable inbox schema * inbox: use separate model * remove old code that used group.chat * add back chat field (not used) and remove old tests * remove inbox exclusions when loading user * add GET /api/v3/inbox/messages * add comment * implement DELETE /inbox/messages/:messageid in v4 * implement GET /inbox/messages in v4 and update tests * implement DELETE /api/v4/inbox/clear * fix url * fix doc * update /export/inbox.html * update other data exports * add back messages in user schema * add user.toJSONWithInbox * add compativility until migration is done * more compatibility * fix tojson called twice * add compatibility methods * fix common tests * fix v4 integration tests * v3 get user -> with inbox * start to fix tests * fix v3 integration tests * wip * wip, client use new route * update tests for members/send-private-message * tests for get user in v4 * add tests for DELETE /inbox/messages/:messageId * add tests for DELETE /inbox/clear in v4 * update docs * fix tests * initial migration * fix migration * fix migration * migration fixes * migrate api.enterCouponCode * migrate api.castSpell * migrate reset, reroll, rebirth * add routes to v4 version * fix tests * fixes * api.updateUser * remove .only * get user -> userLib * refactor inbox.vue to work with new data model * fix return message when messaging yourself * wip fix bug with new conversation * wip * fix remaining ui issues * move api.registerLocal, fixes * keep only v3 version of GET /inbox/messages
This commit is contained in:
123
migrations/20180811_inboxOutsideUser.js
Normal file
123
migrations/20180811_inboxOutsideUser.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
const migrationName = '20180811_inboxOutsideUser.js';
|
||||||
|
const authorName = 'paglias'; // in case script author needs to know when their ...
|
||||||
|
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Move inbox messages from the user model to their own collection
|
||||||
|
*/
|
||||||
|
|
||||||
|
const monk = require('monk');
|
||||||
|
const nconf = require('nconf');
|
||||||
|
|
||||||
|
const Inbox = require('../website/server/models/message').inboxModel;
|
||||||
|
const connectionString = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
|
||||||
|
const dbInboxes = monk(connectionString).get('inboxes', { castIds: false });
|
||||||
|
const dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||||
|
|
||||||
|
function processUsers (lastId) {
|
||||||
|
let query = {
|
||||||
|
migration: {$ne: migrationName},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastId) {
|
||||||
|
query._id = {
|
||||||
|
$gt: lastId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUsers.find(query, {
|
||||||
|
sort: {_id: 1},
|
||||||
|
limit: 1000,
|
||||||
|
fields: ['_id', 'inbox'],
|
||||||
|
})
|
||||||
|
.then(updateUsers)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
return exiting(1, `ERROR! ${ err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let progressCount = 1000;
|
||||||
|
let count = 0;
|
||||||
|
let msgCount = 0;
|
||||||
|
|
||||||
|
function updateUsers (users) {
|
||||||
|
if (!users || users.length === 0) {
|
||||||
|
console.warn('All appropriate users and their tasks found and modified.');
|
||||||
|
displayData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let usersPromises = users.map(updateUser);
|
||||||
|
let lastUser = users[users.length - 1];
|
||||||
|
|
||||||
|
return Promise.all(usersPromises)
|
||||||
|
.then(() => {
|
||||||
|
return processUsers(lastUser._id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUser (user) {
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
|
||||||
|
if (msgCount % progressCount === 0) console.warn(`${msgCount } messages processed`);
|
||||||
|
if (user._id === authorUuid) console.warn(`${authorName } being processed`);
|
||||||
|
|
||||||
|
const oldInboxMessages = user.inbox.messages || {};
|
||||||
|
const oldInboxMessagesIds = Object.keys(oldInboxMessages);
|
||||||
|
|
||||||
|
msgCount += oldInboxMessagesIds.length;
|
||||||
|
|
||||||
|
const newInboxMessages = oldInboxMessagesIds.map(msgId => {
|
||||||
|
const msg = oldInboxMessages[msgId];
|
||||||
|
if (!msg || (!msg.id && !msg._id)) { // eslint-disable-line no-extra-parens
|
||||||
|
console.log('missing message or message _id and id', msg);
|
||||||
|
throw new Error('error!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.id && !msg._id) msg._id = msg.id;
|
||||||
|
if (msg._id && !msg.id) msg.id = msg._id;
|
||||||
|
|
||||||
|
const newMsg = new Inbox(msg);
|
||||||
|
newMsg.ownerId = user._id;
|
||||||
|
return newMsg.toJSON();
|
||||||
|
});
|
||||||
|
|
||||||
|
return dbInboxes.insert(newInboxMessages)
|
||||||
|
.then(() => {
|
||||||
|
return dbUsers.update({_id: user._id}, {
|
||||||
|
$set: {
|
||||||
|
migration: migrationName,
|
||||||
|
'inbox.messages': {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
return exiting(1, `ERROR! ${ err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayData () {
|
||||||
|
console.warn(`\n${ count } users processed\n`);
|
||||||
|
console.warn(`\n${ msgCount } messages processed\n`);
|
||||||
|
return exiting(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exiting (code, msg) {
|
||||||
|
code = code || 0; // 0 = success
|
||||||
|
if (code && !msg) {
|
||||||
|
msg = 'ERROR!';
|
||||||
|
}
|
||||||
|
if (msg) {
|
||||||
|
if (code) {
|
||||||
|
console.error(msg);
|
||||||
|
} else {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = processUsers;
|
||||||
@@ -1020,32 +1020,6 @@ describe('Group Model', () => {
|
|||||||
expect(chat.user).to.not.exist;
|
expect(chat.user).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cuts down chat to 200 messages', () => {
|
|
||||||
for (let i = 0; i < 220; i++) {
|
|
||||||
party.chat.push({ text: 'a message' });
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(party.chat).to.have.a.lengthOf(220);
|
|
||||||
|
|
||||||
party.sendChat('message');
|
|
||||||
|
|
||||||
expect(party.chat).to.have.a.lengthOf(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cuts down chat to 400 messages when group is subcribed', () => {
|
|
||||||
party.purchased.plan.customerId = 'test-customer-id';
|
|
||||||
|
|
||||||
for (let i = 0; i < 420; i++) {
|
|
||||||
party.chat.push({ text: 'a message' });
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(party.chat).to.have.a.lengthOf(420);
|
|
||||||
|
|
||||||
party.sendChat('message');
|
|
||||||
|
|
||||||
expect(party.chat).to.have.a.lengthOf(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates users about new messages in party', () => {
|
it('updates users about new messages in party', () => {
|
||||||
party.sendChat('message');
|
party.sendChat('message');
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ describe('GET /inbox/messages', () => {
|
|||||||
|
|
||||||
// message to yourself
|
// message to yourself
|
||||||
expect(messages[0].text).to.equal('fourth');
|
expect(messages[0].text).to.equal('fourth');
|
||||||
|
expect(messages[0].sent).to.equal(false);
|
||||||
expect(messages[0].uuid).to.equal(user._id);
|
expect(messages[0].uuid).to.equal(user._id);
|
||||||
|
|
||||||
expect(messages[1].text).to.equal('third');
|
expect(messages[1].text).to.equal('third');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
generateUser,
|
generateUser,
|
||||||
sleep,
|
sleep,
|
||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
import { model as Chat } from '../../../../../website/server/models/chat';
|
import { chatModel as Chat } from '../../../../../website/server/models/message';
|
||||||
|
|
||||||
describe('POST /groups/:groupId/quests/accept', () => {
|
describe('POST /groups/:groupId/quests/accept', () => {
|
||||||
const PET_QUEST = 'whale';
|
const PET_QUEST = 'whale';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
generateUser,
|
generateUser,
|
||||||
sleep,
|
sleep,
|
||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
import { model as Chat } from '../../../../../website/server/models/chat';
|
import { chatModel as Chat } from '../../../../../website/server/models/message';
|
||||||
|
|
||||||
describe('POST /groups/:groupId/quests/force-start', () => {
|
describe('POST /groups/:groupId/quests/force-start', () => {
|
||||||
const PET_QUEST = 'whale';
|
const PET_QUEST = 'whale';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||||
import { model as Chat } from '../../../../../website/server/models/chat';
|
import { chatModel as Chat } from '../../../../../website/server/models/message';
|
||||||
import apiError from '../../../../../website/server/libs/apiError';
|
import apiError from '../../../../../website/server/libs/apiError';
|
||||||
|
|
||||||
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
|
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
sleep,
|
sleep,
|
||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
import { model as Chat } from '../../../../../website/server/models/chat';
|
import { chatModel as Chat } from '../../../../../website/server/models/message';
|
||||||
|
|
||||||
describe('POST /groups/:groupId/quests/reject', () => {
|
describe('POST /groups/:groupId/quests/reject', () => {
|
||||||
let questingGroup;
|
let questingGroup;
|
||||||
|
|||||||
62
test/api/v4/coupon/POST-coupons_enter_code.test.js
Normal file
62
test/api/v4/coupon/POST-coupons_enter_code.test.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
resetHabiticaDB,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
describe('POST /coupons/enter/:code', () => {
|
||||||
|
let user;
|
||||||
|
let sudoUser;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await resetHabiticaDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
sudoUser = await generateUser({
|
||||||
|
'contributor.sudo': true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if code is missing', async () => {
|
||||||
|
await expect(user.post('/coupons/enter')).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: 'Not found.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if code is invalid', async () => {
|
||||||
|
await expect(user.post('/coupons/enter/notValid')).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidCoupon'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if coupon has been used', async () => {
|
||||||
|
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
|
||||||
|
await user.post(`/coupons/enter/${coupon._id}`); // use coupon
|
||||||
|
|
||||||
|
await expect(user.post(`/coupons/enter/${coupon._id}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('couponUsed'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply the coupon to the user', async () => {
|
||||||
|
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
|
||||||
|
let userRes = await user.post(`/coupons/enter/${coupon._id}`);
|
||||||
|
expect(userRes._id).to.equal(user._id);
|
||||||
|
expect(userRes.items.gear.owned.eyewear_special_wondercon_red).to.be.true;
|
||||||
|
expect(userRes.items.gear.owned.eyewear_special_wondercon_black).to.be.true;
|
||||||
|
expect(userRes.items.gear.owned.back_special_wondercon_black).to.be.true;
|
||||||
|
expect(userRes.items.gear.owned.back_special_wondercon_red).to.be.true;
|
||||||
|
expect(userRes.items.gear.owned.body_special_wondercon_red).to.be.true;
|
||||||
|
expect(userRes.items.gear.owned.body_special_wondercon_black).to.be.true;
|
||||||
|
expect(userRes.items.gear.owned.body_special_wondercon_gold).to.be.true;
|
||||||
|
expect(userRes.extra).to.eql({signupEvent: 'wondercon'});
|
||||||
|
});
|
||||||
|
});
|
||||||
30
test/api/v4/inbox/DELETE-inbox_clear.test.js
Normal file
30
test/api/v4/inbox/DELETE-inbox_clear.test.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
describe('DELETE /inbox/clear', () => {
|
||||||
|
it('removes all inbox messages for the user', async () => {
|
||||||
|
const [user, otherUser] = await Promise.all([generateUser(), generateUser()]);
|
||||||
|
|
||||||
|
await otherUser.post('/members/send-private-message', {
|
||||||
|
toUserId: user.id,
|
||||||
|
message: 'first',
|
||||||
|
});
|
||||||
|
await user.post('/members/send-private-message', {
|
||||||
|
toUserId: otherUser.id,
|
||||||
|
message: 'second',
|
||||||
|
});
|
||||||
|
await otherUser.post('/members/send-private-message', {
|
||||||
|
toUserId: user.id,
|
||||||
|
message: 'third',
|
||||||
|
});
|
||||||
|
|
||||||
|
let messages = await user.get('/inbox/messages');
|
||||||
|
expect(messages.length).to.equal(3);
|
||||||
|
|
||||||
|
await user.del('/inbox/clear/');
|
||||||
|
|
||||||
|
messages = await user.get('/inbox/messages');
|
||||||
|
expect(messages.length).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
58
test/api/v4/user/GET-user.test.js
Normal file
58
test/api/v4/user/GET-user.test.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
import common from '../../../../website/common';
|
||||||
|
|
||||||
|
describe('GET /user', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the authenticated user with computed stats', async () => {
|
||||||
|
let returnedUser = await user.get('/user');
|
||||||
|
expect(returnedUser._id).to.equal(user._id);
|
||||||
|
|
||||||
|
expect(returnedUser.stats.maxMP).to.exist;
|
||||||
|
expect(returnedUser.stats.maxHealth).to.equal(common.maxHealth);
|
||||||
|
expect(returnedUser.stats.toNextLevel).to.equal(common.tnl(returnedUser.stats.lvl));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return private paths (and apiToken)', async () => {
|
||||||
|
let returnedUser = await user.get('/user');
|
||||||
|
|
||||||
|
expect(returnedUser.auth.local.hashed_password).to.not.exist;
|
||||||
|
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
|
||||||
|
expect(returnedUser.auth.local.salt).to.not.exist;
|
||||||
|
expect(returnedUser.apiToken).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns only user properties requested', async () => {
|
||||||
|
let returnedUser = await user.get('/user?userFields=achievements,items.mounts');
|
||||||
|
|
||||||
|
expect(returnedUser._id).to.equal(user._id);
|
||||||
|
expect(returnedUser.achievements).to.exist;
|
||||||
|
expect(returnedUser.items.mounts).to.exist;
|
||||||
|
// Notifications are always returned
|
||||||
|
expect(returnedUser.notifications).to.exist;
|
||||||
|
expect(returnedUser.stats).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return new inbox messages', async () => {
|
||||||
|
const otherUser = await generateUser();
|
||||||
|
|
||||||
|
await otherUser.post('/members/send-private-message', {
|
||||||
|
toUserId: user.id,
|
||||||
|
message: 'first',
|
||||||
|
});
|
||||||
|
await otherUser.post('/members/send-private-message', {
|
||||||
|
toUserId: user.id,
|
||||||
|
message: 'second',
|
||||||
|
});
|
||||||
|
let returnedUser = await user.get('/user');
|
||||||
|
|
||||||
|
expect(returnedUser._id).to.equal(user._id);
|
||||||
|
expect(returnedUser.inbox.messages).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
324
test/api/v4/user/POST-user_class_cast_spellId.test.js
Normal file
324
test/api/v4/user/POST-user_class_cast_spellId.test.js
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
createAndPopulateGroup,
|
||||||
|
generateGroup,
|
||||||
|
generateChallenge,
|
||||||
|
sleep,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
import { find } from 'lodash';
|
||||||
|
import apiError from '../../../../website/server/libs/apiError';
|
||||||
|
|
||||||
|
describe('POST /user/class/cast/:spellId', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if spell does not exist', async () => {
|
||||||
|
await user.update({'stats.class': 'rogue'});
|
||||||
|
let spellId = 'invalidSpell';
|
||||||
|
await expect(user.post(`/user/class/cast/${spellId}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: apiError('spellNotFound', {spellId}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if spell does not exist in user\'s class', async () => {
|
||||||
|
let spellId = 'pickPocket';
|
||||||
|
await expect(user.post(`/user/class/cast/${spellId}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: apiError('spellNotFound', {spellId}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if spell.mana > user.mana', async () => {
|
||||||
|
await user.update({'stats.class': 'rogue'});
|
||||||
|
await expect(user.post('/user/class/cast/backStab'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('notEnoughMana'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if spell.value > user.gold', async () => {
|
||||||
|
await expect(user.post('/user/class/cast/birthday'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('messageNotEnoughGold'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if spell.lvl > user.level', async () => {
|
||||||
|
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
|
||||||
|
await expect(user.post('/user/class/cast/earth'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('spellLevelTooHigh', {level: 13}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if user doesn\'t own the spell', async () => {
|
||||||
|
await expect(user.post('/user/class/cast/snowball'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('spellNotOwned'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if targetId is not an UUID', async () => {
|
||||||
|
await expect(user.post('/user/class/cast/spellId?targetId=notAnUUID'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if targetId is required but missing', async () => {
|
||||||
|
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
|
||||||
|
await expect(user.post('/user/class/cast/pickPocket'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('targetIdUUID'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if targeted task doesn\'t exist', async () => {
|
||||||
|
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
|
||||||
|
await expect(user.post(`/user/class/cast/pickPocket?targetId=${generateUUID()}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('taskNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if a challenge task was targeted', async () => {
|
||||||
|
let {group, groupLeader} = await createAndPopulateGroup();
|
||||||
|
let challenge = await generateChallenge(groupLeader, group);
|
||||||
|
await groupLeader.post(`/challenges/${challenge._id}/join`);
|
||||||
|
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
|
||||||
|
{type: 'habit', text: 'task text'},
|
||||||
|
]);
|
||||||
|
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
|
||||||
|
await sleep(0.5);
|
||||||
|
await groupLeader.sync();
|
||||||
|
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupLeader.tasksOrder.habits[0]}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('challengeTasksNoCast'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if a group task was targeted', async () => {
|
||||||
|
let {group, groupLeader} = await createAndPopulateGroup();
|
||||||
|
|
||||||
|
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
|
||||||
|
text: 'todo group',
|
||||||
|
type: 'todo',
|
||||||
|
});
|
||||||
|
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
|
||||||
|
let memberTasks = await groupLeader.get('/tasks/user');
|
||||||
|
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||||
|
return memberTask.group.id === group._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
|
||||||
|
await sleep(0.5);
|
||||||
|
await groupLeader.sync();
|
||||||
|
|
||||||
|
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('groupTasksNoCast'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if targeted party member doesn\'t exist', async () => {
|
||||||
|
let {groupLeader} = await createAndPopulateGroup({
|
||||||
|
groupDetails: { type: 'party', privacy: 'private' },
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
await groupLeader.update({'items.special.snowball': 3});
|
||||||
|
|
||||||
|
let target = generateUUID();
|
||||||
|
await expect(groupLeader.post(`/user/class/cast/snowball?targetId=${target}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('userWithIDNotFound', {userId: target}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if party does not exists', async () => {
|
||||||
|
await user.update({'items.special.snowball': 3});
|
||||||
|
|
||||||
|
await expect(user.post(`/user/class/cast/snowball?targetId=${generateUUID()}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('partyNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('send message in party chat if party && !spell.silent', async () => {
|
||||||
|
let { group, groupLeader } = await createAndPopulateGroup({
|
||||||
|
groupDetails: { type: 'party', privacy: 'private' },
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
|
||||||
|
|
||||||
|
await groupLeader.post('/user/class/cast/earth');
|
||||||
|
await sleep(1);
|
||||||
|
const groupMessages = await groupLeader.get(`/groups/${group._id}/chat`);
|
||||||
|
|
||||||
|
expect(groupMessages[0]).to.exist;
|
||||||
|
expect(groupMessages[0].uuid).to.equal('system');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Ethereal Surge does not recover mp of other mages', async () => {
|
||||||
|
let group = await createAndPopulateGroup({
|
||||||
|
groupDetails: { type: 'party', privacy: 'private' },
|
||||||
|
members: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
let promises = [];
|
||||||
|
promises.push(group.groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 20}));
|
||||||
|
promises.push(group.members[0].update({'stats.mp': 0, 'stats.class': 'warrior', 'stats.lvl': 20}));
|
||||||
|
promises.push(group.members[1].update({'stats.mp': 0, 'stats.class': 'wizard', 'stats.lvl': 20}));
|
||||||
|
promises.push(group.members[2].update({'stats.mp': 0, 'stats.class': 'rogue', 'stats.lvl': 20}));
|
||||||
|
promises.push(group.members[3].update({'stats.mp': 0, 'stats.class': 'healer', 'stats.lvl': 20}));
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
await group.groupLeader.post('/user/class/cast/mpheal');
|
||||||
|
|
||||||
|
promises = [];
|
||||||
|
promises.push(group.members[0].sync());
|
||||||
|
promises.push(group.members[1].sync());
|
||||||
|
promises.push(group.members[2].sync());
|
||||||
|
promises.push(group.members[3].sync());
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
|
||||||
|
expect(group.members[1].stats.mp).to.equal(0); // wizard
|
||||||
|
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
|
||||||
|
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cast bulk', async () => {
|
||||||
|
let { group, groupLeader } = await createAndPopulateGroup({
|
||||||
|
groupDetails: { type: 'party', privacy: 'private' },
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
|
||||||
|
await groupLeader.post('/user/class/cast/earth', {quantity: 2});
|
||||||
|
|
||||||
|
await sleep(1);
|
||||||
|
group = await groupLeader.get(`/groups/${group._id}`);
|
||||||
|
|
||||||
|
expect(group.chat[0]).to.exist;
|
||||||
|
expect(group.chat[0].uuid).to.equal('system');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('searing brightness does not affect challenge or group tasks', async () => {
|
||||||
|
let guild = await generateGroup(user);
|
||||||
|
let challenge = await generateChallenge(user, guild);
|
||||||
|
await user.post(`/challenges/${challenge._id}/join`);
|
||||||
|
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||||
|
text: 'test challenge habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||||
|
text: 'todo group',
|
||||||
|
type: 'todo',
|
||||||
|
});
|
||||||
|
await user.update({'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15});
|
||||||
|
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||||
|
|
||||||
|
await user.post('/user/class/cast/brightness');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
let memberTasks = await user.get('/tasks/user');
|
||||||
|
|
||||||
|
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||||
|
return memberTask.group.id === guild._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||||
|
return memberTask.challenge.id === challenge._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userChallengeTask).to.exist;
|
||||||
|
expect(syncedGroupTask).to.exist;
|
||||||
|
expect(userChallengeTask.value).to.equal(0);
|
||||||
|
expect(syncedGroupTask.value).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increases both user\'s achievement values', async () => {
|
||||||
|
let party = await createAndPopulateGroup({
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
let leader = party.groupLeader;
|
||||||
|
let recipient = party.members[0];
|
||||||
|
await leader.update({'stats.gp': 10});
|
||||||
|
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
|
||||||
|
await leader.sync();
|
||||||
|
await recipient.sync();
|
||||||
|
expect(leader.achievements.birthday).to.equal(1);
|
||||||
|
expect(recipient.achievements.birthday).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only increases user\'s achievement one if target == caster', async () => {
|
||||||
|
await user.update({'stats.gp': 10});
|
||||||
|
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
|
||||||
|
await user.sync();
|
||||||
|
expect(user.achievements.birthday).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes correct target to spell when targetType === \'task\'', async () => {
|
||||||
|
await user.update({'stats.class': 'wizard', 'stats.lvl': 11});
|
||||||
|
|
||||||
|
let task = await user.post('/tasks/user', {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = await user.post(`/user/class/cast/fireball?targetId=${task._id}`);
|
||||||
|
|
||||||
|
expect(result.task._id).to.equal(task._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes correct target to spell when targetType === \'self\'', async () => {
|
||||||
|
await user.update({'stats.class': 'wizard', 'stats.lvl': 14, 'stats.mp': 50});
|
||||||
|
|
||||||
|
let result = await user.post('/user/class/cast/frost');
|
||||||
|
|
||||||
|
expect(result.user.stats.mp).to.equal(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// TODO find a way to have sinon working in integration tests
|
||||||
|
// it doesn't work when tests are running separately from server
|
||||||
|
it('passes correct target to spell when targetType === \'tasks\'');
|
||||||
|
it('passes correct target to spell when targetType === \'party\'');
|
||||||
|
it('passes correct target to spell when targetType === \'user\'');
|
||||||
|
it('passes correct target to spell when targetType === \'party\' and user is not in a party');
|
||||||
|
it('passes correct target to spell when targetType === \'user\' and user is not in a party');
|
||||||
|
});
|
||||||
60
test/api/v4/user/POST-user_rebirth.test.js
Normal file
60
test/api/v4/user/POST-user_rebirth.test.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
generateDaily,
|
||||||
|
generateReward,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
describe('POST /user/rebirth', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when user balance is too low', async () => {
|
||||||
|
await expect(user.post('/user/rebirth'))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('notEnoughGems'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// More tests in common code unit tests
|
||||||
|
|
||||||
|
it('resets user\'s tasks', async () => {
|
||||||
|
await user.update({
|
||||||
|
balance: 1.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
let daily = await generateDaily({
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'daily',
|
||||||
|
value: 1,
|
||||||
|
streak: 1,
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let reward = await generateReward({
|
||||||
|
text: 'test reward',
|
||||||
|
type: 'reward',
|
||||||
|
value: 1,
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = await user.post('/user/rebirth');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(user.notifications.length).to.equal(1);
|
||||||
|
expect(user.notifications[0].type).to.equal('REBIRTH_ACHIEVEMENT');
|
||||||
|
|
||||||
|
let updatedDaily = await user.get(`/tasks/${daily._id}`);
|
||||||
|
let updatedReward = await user.get(`/tasks/${reward._id}`);
|
||||||
|
|
||||||
|
expect(response.message).to.equal(t('rebirthComplete'));
|
||||||
|
expect(updatedDaily.streak).to.equal(0);
|
||||||
|
expect(updatedDaily.value).to.equal(0);
|
||||||
|
expect(updatedReward.value).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
54
test/api/v4/user/POST-user_reroll.test.js
Normal file
54
test/api/v4/user/POST-user_reroll.test.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
generateDaily,
|
||||||
|
generateReward,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
describe('POST /user/reroll', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when user balance is too low', async () => {
|
||||||
|
await expect(user.post('/user/reroll'))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('notEnoughGems'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// More tests in common code unit tests
|
||||||
|
|
||||||
|
it('resets user\'s tasks', async () => {
|
||||||
|
await user.update({
|
||||||
|
balance: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
let daily = await generateDaily({
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'daily',
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let reward = await generateReward({
|
||||||
|
text: 'test reward',
|
||||||
|
type: 'reward',
|
||||||
|
value: 1,
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = await user.post('/user/reroll');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
let updatedDaily = await user.get(`/tasks/${daily._id}`);
|
||||||
|
let updatedReward = await user.get(`/tasks/${reward._id}`);
|
||||||
|
|
||||||
|
expect(response.message).to.equal(t('fortifyComplete'));
|
||||||
|
expect(updatedDaily.value).to.equal(0);
|
||||||
|
expect(updatedReward.value).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
121
test/api/v4/user/POST-user_reset.test.js
Normal file
121
test/api/v4/user/POST-user_reset.test.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
generateGroup,
|
||||||
|
generateChallenge,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
import { find } from 'lodash';
|
||||||
|
|
||||||
|
describe('POST /user/reset', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
// More tests in common code unit tests
|
||||||
|
|
||||||
|
it('resets user\'s habits', async () => {
|
||||||
|
let task = await user.post('/tasks/user', {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.post('/user/reset');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('taskNotFound'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.tasksOrder.habits).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets user\'s dailys', async () => {
|
||||||
|
let task = await user.post('/tasks/user', {
|
||||||
|
text: 'test daily',
|
||||||
|
type: 'daily',
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.post('/user/reset');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('taskNotFound'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.tasksOrder.dailys).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets user\'s todos', async () => {
|
||||||
|
let task = await user.post('/tasks/user', {
|
||||||
|
text: 'test todo',
|
||||||
|
type: 'todo',
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.post('/user/reset');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('taskNotFound'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.tasksOrder.todos).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets user\'s rewards', async () => {
|
||||||
|
let task = await user.post('/tasks/user', {
|
||||||
|
text: 'test reward',
|
||||||
|
type: 'reward',
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.post('/user/reset');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('taskNotFound'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.tasksOrder.rewards).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not delete challenge or group tasks', async () => {
|
||||||
|
let guild = await generateGroup(user);
|
||||||
|
let challenge = await generateChallenge(user, guild);
|
||||||
|
await user.post(`/challenges/${challenge._id}/join`);
|
||||||
|
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||||
|
text: 'test challenge habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||||
|
text: 'todo group',
|
||||||
|
type: 'todo',
|
||||||
|
});
|
||||||
|
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||||
|
|
||||||
|
await user.post('/user/reset');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
let memberTasks = await user.get('/tasks/user');
|
||||||
|
|
||||||
|
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||||
|
return memberTask.group.id === guild._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||||
|
return memberTask.challenge.id === challenge._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userChallengeTask).to.exist;
|
||||||
|
expect(syncedGroupTask).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
256
test/api/v4/user/PUT-user.test.js
Normal file
256
test/api/v4/user/PUT-user.test.js
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
import { each, get } from 'lodash';
|
||||||
|
|
||||||
|
describe('PUT /user', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Allowed Operations', () => {
|
||||||
|
it('updates the user', async () => {
|
||||||
|
await user.put('/user', {
|
||||||
|
'profile.name': 'Frodo',
|
||||||
|
'preferences.costume': true,
|
||||||
|
'stats.hp': 14,
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(user.profile.name).to.eql('Frodo');
|
||||||
|
expect(user.preferences.costume).to.eql(true);
|
||||||
|
expect(user.stats.hp).to.eql(14);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tags must be an array', async () => {
|
||||||
|
await expect(user.put('/user', {
|
||||||
|
tags: {
|
||||||
|
tag: true,
|
||||||
|
},
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'mustBeArray',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update tags', async () => {
|
||||||
|
let userTags = user.tags;
|
||||||
|
|
||||||
|
await user.put('/user', {
|
||||||
|
tags: [...user.tags, {
|
||||||
|
name: 'new tag',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(user.tags.length).to.be.eql(userTags.length + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('profile.name cannot be an empty string or null', async () => {
|
||||||
|
await expect(user.put('/user', {
|
||||||
|
'profile.name': ' ', // string should be trimmed
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'User validation failed',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(user.put('/user', {
|
||||||
|
'profile.name': '',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'User validation failed',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(user.put('/user', {
|
||||||
|
'profile.name': null,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'User validation failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Top Level Protected Operations', () => {
|
||||||
|
let protectedOperations = {
|
||||||
|
'gem balance': {balance: 100},
|
||||||
|
auth: {'auth.blocked': true, 'auth.timestamps.created': new Date()},
|
||||||
|
contributor: {'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text'},
|
||||||
|
backer: {'backer.tier': 10, 'backer.npc': 'Bilbo'},
|
||||||
|
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
|
||||||
|
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
|
||||||
|
notifications: [{type: 123}],
|
||||||
|
webhooks: {webhooks: [{url: 'https://foobar.com'}]},
|
||||||
|
};
|
||||||
|
|
||||||
|
each(protectedOperations, (data, testName) => {
|
||||||
|
it(`does not allow updating ${testName}`, async () => {
|
||||||
|
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
|
||||||
|
|
||||||
|
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: errorText,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Sub-Level Protected Operations', () => {
|
||||||
|
let protectedOperations = {
|
||||||
|
'class stat': {'stats.class': 'wizard'},
|
||||||
|
'flags unless whitelisted': {'flags.dropsEnabled': true},
|
||||||
|
webhooks: {'preferences.webhooks': [1, 2, 3]},
|
||||||
|
sleep: {'preferences.sleep': true},
|
||||||
|
'disable classes': {'preferences.disableClasses': true},
|
||||||
|
};
|
||||||
|
|
||||||
|
each(protectedOperations, (data, testName) => {
|
||||||
|
it(`does not allow updating ${testName}`, async () => {
|
||||||
|
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
|
||||||
|
|
||||||
|
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: errorText,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Default Appearance Preferences', () => {
|
||||||
|
let testCases = {
|
||||||
|
shirt: 'yellow',
|
||||||
|
skin: 'ddc994',
|
||||||
|
'hair.color': 'blond',
|
||||||
|
'hair.bangs': 2,
|
||||||
|
'hair.base': 1,
|
||||||
|
'hair.flower': 4,
|
||||||
|
size: 'broad',
|
||||||
|
};
|
||||||
|
|
||||||
|
each(testCases, (item, type) => {
|
||||||
|
const update = {};
|
||||||
|
update[`preferences.${type}`] = item;
|
||||||
|
|
||||||
|
it(`updates user with ${type} that is a default`, async () => {
|
||||||
|
let dbUpdate = {};
|
||||||
|
dbUpdate[`purchased.${type}.${item}`] = true;
|
||||||
|
await user.update(dbUpdate);
|
||||||
|
|
||||||
|
// Sanity checks to make sure user is not already equipped with item
|
||||||
|
expect(get(user.preferences, type)).to.not.eql(item);
|
||||||
|
|
||||||
|
let updatedUser = await user.put('/user', update);
|
||||||
|
|
||||||
|
expect(get(updatedUser.preferences, type)).to.eql(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if user tries to update body size with invalid type', async () => {
|
||||||
|
await expect(user.put('/user', {
|
||||||
|
'preferences.size': 'round',
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('mustPurchaseToSet', { val: 'round', key: 'preferences.size' }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can set beard to default', async () => {
|
||||||
|
await user.update({
|
||||||
|
'purchased.hair.beard': 3,
|
||||||
|
'preferences.hair.beard': 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedUser = await user.put('/user', {
|
||||||
|
'preferences.hair.beard': 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedUser.preferences.hair.beard).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can set mustache to default', async () => {
|
||||||
|
await user.update({
|
||||||
|
'purchased.hair.mustache': 2,
|
||||||
|
'preferences.hair.mustache': 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedUser = await user.put('/user', {
|
||||||
|
'preferences.hair.mustache': 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedUser.preferences.hair.mustache).to.eql(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Purchasable Appearance Preferences', () => {
|
||||||
|
let testCases = {
|
||||||
|
background: 'volcano',
|
||||||
|
shirt: 'convict',
|
||||||
|
skin: 'cactus',
|
||||||
|
'hair.base': 7,
|
||||||
|
'hair.beard': 2,
|
||||||
|
'hair.color': 'rainbow',
|
||||||
|
'hair.mustache': 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
each(testCases, (item, type) => {
|
||||||
|
const update = {};
|
||||||
|
update[`preferences.${type}`] = item;
|
||||||
|
|
||||||
|
it(`returns an error if user tries to update ${type} with ${type} the user does not own`, async () => {
|
||||||
|
await expect(user.put('/user', update)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('mustPurchaseToSet', {val: item, key: `preferences.${type}`}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`updates user with ${type} user does own`, async () => {
|
||||||
|
let dbUpdate = {};
|
||||||
|
dbUpdate[`purchased.${type}.${item}`] = true;
|
||||||
|
await user.update(dbUpdate);
|
||||||
|
|
||||||
|
// Sanity check to make sure user is not already equipped with item
|
||||||
|
expect(get(user.preferences, type)).to.not.eql(item);
|
||||||
|
|
||||||
|
let updatedUser = await user.put('/user', update);
|
||||||
|
|
||||||
|
expect(get(updatedUser.preferences, type)).to.eql(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('Improvement Categories', () => {
|
||||||
|
it('sets valid categories', async () => {
|
||||||
|
await user.put('/user', {
|
||||||
|
'preferences.improvementCategories': ['work', 'school'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(user.preferences.improvementCategories).to.eql(['work', 'school']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discards invalid categories', async () => {
|
||||||
|
await expect(user.put('/user', {
|
||||||
|
'preferences.improvementCategories': ['work', 'procrastination', 'school'],
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'User validation failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
739
test/api/v4/user/auth/POST-register_local.test.js
Normal file
739
test/api/v4/user/auth/POST-register_local.test.js
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
requester,
|
||||||
|
translate as t,
|
||||||
|
createAndPopulateGroup,
|
||||||
|
getProperty,
|
||||||
|
} from '../../../../helpers/api-integration/v4';
|
||||||
|
import { ApiUser } from '../../../../helpers/api-integration/api-classes';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { each } from 'lodash';
|
||||||
|
import { encrypt } from '../../../../../website/server/libs/encryption';
|
||||||
|
|
||||||
|
function generateRandomUserName () {
|
||||||
|
return (Date.now() + uuid()).substring(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('POST /user/auth/local/register', () => {
|
||||||
|
context('username and email are free', () => {
|
||||||
|
let api;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
api = requester();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('registers a new user', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user._id).to.exist;
|
||||||
|
expect(user.apiToken).to.exist;
|
||||||
|
expect(user.auth.local.username).to.eql(username);
|
||||||
|
expect(user.profile.name).to.eql(username);
|
||||||
|
expect(user.newUser).to.eql(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
xit('remove spaces from username', async () => {
|
||||||
|
// TODO can probably delete this test now
|
||||||
|
let username = ' usernamewithspaces ';
|
||||||
|
let email = 'test@example.com';
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.auth.local.username).to.eql(username.trim());
|
||||||
|
expect(user.profile.name).to.eql(username.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
context('validates username', () => {
|
||||||
|
const email = 'test@example.com';
|
||||||
|
const password = 'password';
|
||||||
|
|
||||||
|
it('requires to username to be less than 20', async () => {
|
||||||
|
const username = (Date.now() + uuid()).substring(0, 21);
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects chracters not in [-_a-zA-Z0-9]', async () => {
|
||||||
|
const username = 'a-zA_Z09*';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows only [-_a-zA-Z0-9] characters', async () => {
|
||||||
|
const username = 'a-zA_Z09';
|
||||||
|
|
||||||
|
const user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.auth.local.username).to.eql(username);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('provides default tags and tasks', async () => {
|
||||||
|
it('for a generic API consumer', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let dailys = await requests.get('/tasks/user?type=dailys');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
let rewards = await requests.get('/tasks/user?type=rewards');
|
||||||
|
let tags = await requests.get('/tags');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(0);
|
||||||
|
expect(dailys).to.have.a.lengthOf(0);
|
||||||
|
expect(todos).to.have.a.lengthOf(1);
|
||||||
|
expect(rewards).to.have.a.lengthOf(0);
|
||||||
|
|
||||||
|
expect(tags).to.have.a.lengthOf(7);
|
||||||
|
expect(tags[0].name).to.eql(t('defaultTag1'));
|
||||||
|
expect(tags[1].name).to.eql(t('defaultTag2'));
|
||||||
|
expect(tags[2].name).to.eql(t('defaultTag3'));
|
||||||
|
expect(tags[3].name).to.eql(t('defaultTag4'));
|
||||||
|
expect(tags[4].name).to.eql(t('defaultTag5'));
|
||||||
|
expect(tags[5].name).to.eql(t('defaultTag6'));
|
||||||
|
expect(tags[6].name).to.eql(t('defaultTag7'));
|
||||||
|
});
|
||||||
|
|
||||||
|
xit('for Web', async () => {
|
||||||
|
api = requester(
|
||||||
|
null,
|
||||||
|
{'x-client': 'habitica-web'},
|
||||||
|
);
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let dailys = await requests.get('/tasks/user?type=dailys');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
let rewards = await requests.get('/tasks/user?type=rewards');
|
||||||
|
let tags = await requests.get('/tags');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(3);
|
||||||
|
expect(habits[0].text).to.eql(t('defaultHabit1Text'));
|
||||||
|
expect(habits[0].notes).to.eql('');
|
||||||
|
expect(habits[1].text).to.eql(t('defaultHabit2Text'));
|
||||||
|
expect(habits[1].notes).to.eql('');
|
||||||
|
expect(habits[2].text).to.eql(t('defaultHabit3Text'));
|
||||||
|
expect(habits[2].notes).to.eql('');
|
||||||
|
|
||||||
|
expect(dailys).to.have.a.lengthOf(0);
|
||||||
|
|
||||||
|
expect(todos).to.have.a.lengthOf(1);
|
||||||
|
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
|
||||||
|
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
|
||||||
|
|
||||||
|
expect(rewards).to.have.a.lengthOf(1);
|
||||||
|
expect(rewards[0].text).to.eql(t('defaultReward1Text'));
|
||||||
|
expect(rewards[0].notes).to.eql('');
|
||||||
|
|
||||||
|
expect(tags).to.have.a.lengthOf(7);
|
||||||
|
expect(tags[0].name).to.eql(t('defaultTag1'));
|
||||||
|
expect(tags[1].name).to.eql(t('defaultTag2'));
|
||||||
|
expect(tags[2].name).to.eql(t('defaultTag3'));
|
||||||
|
expect(tags[3].name).to.eql(t('defaultTag4'));
|
||||||
|
expect(tags[4].name).to.eql(t('defaultTag5'));
|
||||||
|
expect(tags[5].name).to.eql(t('defaultTag6'));
|
||||||
|
expect(tags[6].name).to.eql(t('defaultTag7'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('does not provide default tags and tasks', async () => {
|
||||||
|
it('for Android', async () => {
|
||||||
|
api = requester(
|
||||||
|
null,
|
||||||
|
{'x-client': 'habitica-android'},
|
||||||
|
);
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let dailys = await requests.get('/tasks/user?type=dailys');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
let rewards = await requests.get('/tasks/user?type=rewards');
|
||||||
|
let tags = await requests.get('/tags');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(0);
|
||||||
|
expect(dailys).to.have.a.lengthOf(0);
|
||||||
|
expect(todos).to.have.a.lengthOf(0);
|
||||||
|
expect(rewards).to.have.a.lengthOf(0);
|
||||||
|
expect(tags).to.have.a.lengthOf(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for iOS', async () => {
|
||||||
|
api = requester(
|
||||||
|
null,
|
||||||
|
{'x-client': 'habitica-ios'},
|
||||||
|
);
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let dailys = await requests.get('/tasks/user?type=dailys');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
let rewards = await requests.get('/tasks/user?type=rewards');
|
||||||
|
let tags = await requests.get('/tags');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(0);
|
||||||
|
expect(dailys).to.have.a.lengthOf(0);
|
||||||
|
expect(todos).to.have.a.lengthOf(0);
|
||||||
|
expect(rewards).to.have.a.lengthOf(0);
|
||||||
|
expect(tags).to.have.a.lengthOf(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enrolls new users in an A/B test', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes items awarded by default when creating a new user', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.items.quests.dustbunnies).to.equal(1);
|
||||||
|
expect(user.purchased.background.violet).to.be.ok;
|
||||||
|
expect(user.preferences.background).to.equal('violet');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires password and confirmPassword to match', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
let confirmPassword = 'not password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires a username', async () => {
|
||||||
|
let email = `${generateRandomUserName()}@example.com`;
|
||||||
|
let password = 'password';
|
||||||
|
let confirmPassword = 'password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires an email', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires a valid email', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = 'notanemail@sdf';
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sanitizes email params to a lowercase string before creating the user', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = 'ISANEmAiL@ExAmPle.coM';
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.auth.local.email).to.equal(email.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails on a habitica.com email', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@habitica.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'User validation failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails on a habitrpg.com email', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@habitrpg.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'User validation failed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires a password', async () => {
|
||||||
|
let username = generateRandomUserName();
|
||||||
|
let email = `${username}@example.com`;
|
||||||
|
let confirmPassword = 'password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
confirmPassword,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('attach to facebook user', () => {
|
||||||
|
let user;
|
||||||
|
let email = 'some@email.net';
|
||||||
|
let username = 'some-username';
|
||||||
|
let password = 'some-password';
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
it('checks onlySocialAttachLocal', async () => {
|
||||||
|
await expect(user.post('/user/auth/local/register', {
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('onlySocialAttachLocal'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('succeeds', async () => {
|
||||||
|
await user.update({ 'auth.facebook.id': 'some-fb-id', 'auth.local': { ok: true } });
|
||||||
|
await user.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
await user.sync();
|
||||||
|
expect(user.auth.local.username).to.eql(username);
|
||||||
|
expect(user.auth.local.email).to.eql(email);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('login is already taken', () => {
|
||||||
|
let username, email, api;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
api = requester();
|
||||||
|
username = generateRandomUserName();
|
||||||
|
email = `${username}@example.com`;
|
||||||
|
|
||||||
|
return generateUser({
|
||||||
|
'auth.local.username': username,
|
||||||
|
'auth.local.lowerCaseUsername': username,
|
||||||
|
'auth.local.email': email,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects if username is already taken', async () => {
|
||||||
|
let uniqueEmail = `${generateRandomUserName()}@exampe.com`;
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email: uniqueEmail,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('usernameTaken'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects if email is already taken', async () => {
|
||||||
|
let uniqueUsername = generateRandomUserName();
|
||||||
|
let password = 'password';
|
||||||
|
|
||||||
|
await expect(api.post('/user/auth/local/register', {
|
||||||
|
username: uniqueUsername,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('emailTaken'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('req.query.groupInvite', () => {
|
||||||
|
let api, username, email, password;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = requester();
|
||||||
|
username = generateRandomUserName();
|
||||||
|
email = `${username}@example.com`;
|
||||||
|
password = 'password';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not crash the signup process when it\'s invalid', async () => {
|
||||||
|
let user = await api.post('/user/auth/local/register?groupInvite=aaaaInvalid', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user._id).to.be.a('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports invite using req.query.groupInvite', async () => {
|
||||||
|
let { group, groupLeader } = await createAndPopulateGroup({
|
||||||
|
groupDetails: { type: 'party', privacy: 'private' },
|
||||||
|
});
|
||||||
|
|
||||||
|
let invite = encrypt(JSON.stringify({
|
||||||
|
id: group._id,
|
||||||
|
inviter: groupLeader._id,
|
||||||
|
sentAt: Date.now(), // so we can let it expire
|
||||||
|
}));
|
||||||
|
|
||||||
|
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.invitations.parties[0].id).to.eql(group._id);
|
||||||
|
expect(user.invitations.parties[0].name).to.eql(group.name);
|
||||||
|
expect(user.invitations.parties[0].inviter).to.eql(groupLeader._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('awards achievement to inviter', async () => {
|
||||||
|
let { group, groupLeader } = await createAndPopulateGroup({
|
||||||
|
groupDetails: { type: 'party', privacy: 'private' },
|
||||||
|
});
|
||||||
|
|
||||||
|
let invite = encrypt(JSON.stringify({
|
||||||
|
id: group._id,
|
||||||
|
inviter: groupLeader._id,
|
||||||
|
sentAt: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupLeader.sync();
|
||||||
|
expect(groupLeader.achievements.invitedFriend).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user not added to a party on expired invite', async () => {
|
||||||
|
let { group, groupLeader } = await createAndPopulateGroup({
|
||||||
|
groupDetails: { type: 'party', privacy: 'private' },
|
||||||
|
});
|
||||||
|
|
||||||
|
let invite = encrypt(JSON.stringify({
|
||||||
|
id: group._id,
|
||||||
|
inviter: groupLeader._id,
|
||||||
|
sentAt: Date.now() - 6.912e8, // 8 days old
|
||||||
|
}));
|
||||||
|
|
||||||
|
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.invitations.party).to.eql({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a user to a guild on an invite of type other than party', async () => {
|
||||||
|
let { group, groupLeader } = await createAndPopulateGroup({
|
||||||
|
groupDetails: { type: 'guild', privacy: 'private' },
|
||||||
|
});
|
||||||
|
|
||||||
|
let invite = encrypt(JSON.stringify({
|
||||||
|
id: group._id,
|
||||||
|
inviter: groupLeader._id,
|
||||||
|
sentAt: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.invitations.guilds[0]).to.eql({
|
||||||
|
id: group._id,
|
||||||
|
name: group.name,
|
||||||
|
inviter: groupLeader._id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('successful login via api', () => {
|
||||||
|
let api, username, email, password;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = requester();
|
||||||
|
username = generateRandomUserName();
|
||||||
|
email = `${username}@example.com`;
|
||||||
|
password = 'password';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets all site tour values to -2 (already seen)', async () => {
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.flags.tour).to.not.be.empty;
|
||||||
|
|
||||||
|
each(user.flags.tour, (value) => {
|
||||||
|
expect(value).to.eql(-2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('populates user with default todos, not no other task types', async () => {
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.tasksOrder.todos).to.not.be.empty;
|
||||||
|
expect(user.tasksOrder.dailys).to.be.empty;
|
||||||
|
expect(user.tasksOrder.habits).to.be.empty;
|
||||||
|
expect(user.tasksOrder.rewards).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('populates user with default tags', async () => {
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.tags).to.not.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('successful login with habitica-web header', () => {
|
||||||
|
let api, username, email, password;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
api = requester({}, {'x-client': 'habitica-web'});
|
||||||
|
username = generateRandomUserName();
|
||||||
|
email = `${username}@example.com`;
|
||||||
|
password = 'password';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets all common tutorial flags to true', async () => {
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.flags.tour).to.not.be.empty;
|
||||||
|
|
||||||
|
each(user.flags.tutorial.common, (value) => {
|
||||||
|
expect(value).to.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('populates user with default todos, habits, and rewards', async () => {
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.tasksOrder.todos).to.be.empty;
|
||||||
|
expect(user.tasksOrder.dailys).to.be.empty;
|
||||||
|
expect(user.tasksOrder.habits).to.be.empty;
|
||||||
|
expect(user.tasksOrder.rewards).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('populates user with default tags', async () => {
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.tags).to.not.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds the correct tags to the correct tasks', async () => {
|
||||||
|
let user = await api.post('/user/auth/local/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirmPassword: password,
|
||||||
|
});
|
||||||
|
|
||||||
|
let requests = new ApiUser(user);
|
||||||
|
|
||||||
|
let habits = await requests.get('/tasks/user?type=habits');
|
||||||
|
let todos = await requests.get('/tasks/user?type=todos');
|
||||||
|
|
||||||
|
expect(habits).to.have.a.lengthOf(0);
|
||||||
|
expect(todos).to.have.a.lengthOf(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import clearPMs from '../../../website/common/script/ops/clearPMs';
|
|
||||||
import {
|
|
||||||
generateUser,
|
|
||||||
} from '../../helpers/common.helper';
|
|
||||||
|
|
||||||
describe('shared.ops.clearPMs', () => {
|
|
||||||
let user;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
user = generateUser();
|
|
||||||
user.inbox.messages = { first: 'message', second: 'message' };
|
|
||||||
});
|
|
||||||
|
|
||||||
it('clears messages', () => {
|
|
||||||
expect(user.inbox.messages).to.not.eql({});
|
|
||||||
let [result] = clearPMs(user);
|
|
||||||
expect(user.inbox.messages).to.eql({});
|
|
||||||
expect(result).to.eql({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import deletePM from '../../../website/common/script/ops/deletePM';
|
|
||||||
import {
|
|
||||||
generateUser,
|
|
||||||
} from '../../helpers/common.helper';
|
|
||||||
|
|
||||||
describe('shared.ops.deletePM', () => {
|
|
||||||
let user;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
user = generateUser();
|
|
||||||
user.inbox.messages = { first: 'message', second: 'message' };
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delete message', () => {
|
|
||||||
expect(user.inbox.messages).to.not.eql({ second: 'message' });
|
|
||||||
let [response] = deletePM(user, { params: { id: 'first' } });
|
|
||||||
expect(user.inbox.messages).to.eql({ second: 'message' });
|
|
||||||
expect(response).to.eql({ second: 'message' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -152,8 +152,6 @@ import unlock from './ops/unlock';
|
|||||||
import revive from './ops/revive';
|
import revive from './ops/revive';
|
||||||
import rebirth from './ops/rebirth';
|
import rebirth from './ops/rebirth';
|
||||||
import blockUser from './ops/blockUser';
|
import blockUser from './ops/blockUser';
|
||||||
import clearPMs from './ops/clearPMs';
|
|
||||||
import deletePM from './ops/deletePM';
|
|
||||||
import reroll from './ops/reroll';
|
import reroll from './ops/reroll';
|
||||||
import reset from './ops/reset';
|
import reset from './ops/reset';
|
||||||
import markPmsRead from './ops/markPMSRead';
|
import markPmsRead from './ops/markPMSRead';
|
||||||
@@ -182,8 +180,6 @@ api.ops = {
|
|||||||
revive,
|
revive,
|
||||||
rebirth,
|
rebirth,
|
||||||
blockUser,
|
blockUser,
|
||||||
clearPMs,
|
|
||||||
deletePM,
|
|
||||||
reroll,
|
reroll,
|
||||||
reset,
|
reset,
|
||||||
markPmsRead,
|
markPmsRead,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = function clearPMs (user) {
|
|
||||||
user.inbox.messages = {};
|
|
||||||
if (user.markModified) user.markModified('inbox.messages');
|
|
||||||
return [
|
|
||||||
user.inbox.messages,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import get from 'lodash/get';
|
|
||||||
|
|
||||||
module.exports = function deletePM (user, req = {}) {
|
|
||||||
delete user.inbox.messages[get(req, 'params.id')];
|
|
||||||
if (user.markModified) user.markModified(`inbox.messages.${req.params.id}`);
|
|
||||||
return [
|
|
||||||
user.inbox.messages,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
@@ -14,8 +14,6 @@ import addTag from './addTag';
|
|||||||
import sortTag from './sortTag';
|
import sortTag from './sortTag';
|
||||||
import updateTag from './updateTag';
|
import updateTag from './updateTag';
|
||||||
import deleteTag from './deleteTag';
|
import deleteTag from './deleteTag';
|
||||||
import clearPMs from './clearPMs';
|
|
||||||
import deletePM from './deletePM';
|
|
||||||
import blockUser from './blockUser';
|
import blockUser from './blockUser';
|
||||||
import feed from './feed';
|
import feed from './feed';
|
||||||
import releasePets from './releasePets';
|
import releasePets from './releasePets';
|
||||||
@@ -50,8 +48,6 @@ module.exports = {
|
|||||||
sortTag,
|
sortTag,
|
||||||
updateTag,
|
updateTag,
|
||||||
deleteTag,
|
deleteTag,
|
||||||
clearPMs,
|
|
||||||
deletePM,
|
|
||||||
blockUser,
|
blockUser,
|
||||||
feed,
|
feed,
|
||||||
releasePets,
|
releasePets,
|
||||||
|
|||||||
@@ -11,61 +11,22 @@ import {
|
|||||||
NotFound,
|
NotFound,
|
||||||
} from '../../libs/errors';
|
} from '../../libs/errors';
|
||||||
import * as passwordUtils from '../../libs/password';
|
import * as passwordUtils from '../../libs/password';
|
||||||
import logger from '../../libs/logger';
|
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
import { model as Group } from '../../models/group';
|
|
||||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||||
import { sendTxn as sendTxnEmail } from '../../libs/email';
|
import { sendTxn as sendTxnEmail } from '../../libs/email';
|
||||||
import { decrypt, encrypt } from '../../libs/encryption';
|
|
||||||
import { send as sendEmail } from '../../libs/email';
|
import { send as sendEmail } from '../../libs/email';
|
||||||
import pusher from '../../libs/pusher';
|
import pusher from '../../libs/pusher';
|
||||||
import common from '../../../common';
|
import common from '../../../common';
|
||||||
import { validatePasswordResetCodeAndFindUser, convertToBcrypt} from '../../libs/password';
|
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 BASE_URL = nconf.get('BASE_URL');
|
||||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
|
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
|
||||||
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
||||||
const USERNAME_LENGTH_MIN = 1;
|
|
||||||
const USERNAME_LENGTH_MAX = 20;
|
|
||||||
|
|
||||||
let api = {};
|
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) {
|
function hasBackupAuth (user, networkToRemove) {
|
||||||
if (user.auth.local.username) {
|
if (user.auth.local.username) {
|
||||||
return true;
|
return true;
|
||||||
@@ -78,6 +39,8 @@ function hasBackupAuth (user, networkToRemove) {
|
|||||||
return hasAlternateNetwork;
|
return hasAlternateNetwork;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v4 version */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/user/auth/local/register Register
|
* @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
|
* @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',
|
url: '/user/auth/local/register',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let existingUser = res.locals.user; // If adding local auth to social user
|
await authLib.registerLocal(req, res, { isV3: true });
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -398,9 +253,7 @@ api.loginSocial = {
|
|||||||
*/
|
*/
|
||||||
api.pusherAuth = {
|
api.pusherAuth = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/auth/pusher',
|
url: '/user/auth/pusher',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -468,9 +321,7 @@ api.pusherAuth = {
|
|||||||
**/
|
**/
|
||||||
api.updateUsername = {
|
api.updateUsername = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/auth/update-username',
|
url: '/user/auth/update-username',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -524,9 +375,7 @@ api.updateUsername = {
|
|||||||
**/
|
**/
|
||||||
api.updatePassword = {
|
api.updatePassword = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/auth/update-password',
|
url: '/user/auth/update-password',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -636,9 +485,7 @@ api.resetPassword = {
|
|||||||
*/
|
*/
|
||||||
api.updateEmail = {
|
api.updateEmail = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/auth/update-email',
|
url: '/user/auth/update-email',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -725,9 +572,7 @@ api.resetPasswordSetNewOne = {
|
|||||||
api.deleteSocial = {
|
api.deleteSocial = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/user/auth/social/:network',
|
url: '/user/auth/social/:network',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let network = req.params.network;
|
let network = req.params.network;
|
||||||
|
|||||||
@@ -184,9 +184,7 @@ let api = {};
|
|||||||
api.createChallenge = {
|
api.createChallenge = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/challenges',
|
url: '/challenges',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -235,9 +233,7 @@ api.createChallenge = {
|
|||||||
api.joinChallenge = {
|
api.joinChallenge = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/challenges/:challengeId/join',
|
url: '/challenges/:challengeId/join',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -294,9 +290,7 @@ api.joinChallenge = {
|
|||||||
api.leaveChallenge = {
|
api.leaveChallenge = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/challenges/:challengeId/leave',
|
url: '/challenges/:challengeId/leave',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let keep = req.body.keep === 'remove-all' ? 'remove-all' : 'keep-all';
|
let keep = req.body.keep === 'remove-all' ? 'remove-all' : 'keep-all';
|
||||||
@@ -345,9 +339,7 @@ api.leaveChallenge = {
|
|||||||
api.getUserChallenges = {
|
api.getUserChallenges = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/challenges/user',
|
url: '/challenges/user',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
const CHALLENGES_PER_PAGE = 10;
|
const CHALLENGES_PER_PAGE = 10;
|
||||||
const page = req.query.page;
|
const page = req.query.page;
|
||||||
@@ -508,9 +500,7 @@ api.getGroupChallenges = {
|
|||||||
api.getChallenge = {
|
api.getChallenge = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/challenges/:challengeId',
|
url: '/challenges/:challengeId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
@@ -664,9 +654,7 @@ api.exportChallengeCsv = {
|
|||||||
api.updateChallenge = {
|
api.updateChallenge = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: '/challenges/:challengeId',
|
url: '/challenges/:challengeId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
@@ -708,9 +696,7 @@ api.updateChallenge = {
|
|||||||
api.deleteChallenge = {
|
api.deleteChallenge = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/challenges/:challengeId',
|
url: '/challenges/:challengeId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -755,9 +741,7 @@ api.deleteChallenge = {
|
|||||||
api.selectChallengeWinner = {
|
api.selectChallengeWinner = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/challenges/:challengeId/selectWinner/:winnerId',
|
url: '/challenges/:challengeId/selectWinner/:winnerId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -806,9 +790,7 @@ api.selectChallengeWinner = {
|
|||||||
api.cloneChallenge = {
|
api.cloneChallenge = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/challenges/:challengeId/clone',
|
url: '/challenges/:challengeId/clone',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { authWithHeaders } from '../../middlewares/auth';
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
import { model as Chat } from '../../models/chat';
|
import { chatModel as Chat } from '../../models/message';
|
||||||
import {
|
import {
|
||||||
BadRequest,
|
BadRequest,
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -62,9 +62,7 @@ function textContainsBannedSlur (message) {
|
|||||||
api.getChat = {
|
api.getChat = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/groups/:groupId/chat',
|
url: '/groups/:groupId/chat',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -103,9 +101,7 @@ function getBannedWordsFromText (message) {
|
|||||||
api.postChat = {
|
api.postChat = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/chat',
|
url: '/groups/:groupId/chat',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let groupId = req.params.groupId;
|
let groupId = req.params.groupId;
|
||||||
@@ -227,9 +223,7 @@ api.postChat = {
|
|||||||
api.likeChat = {
|
api.likeChat = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/chat/:chatId/like',
|
url: '/groups/:groupId/chat/:chatId/like',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let groupId = req.params.groupId;
|
let groupId = req.params.groupId;
|
||||||
@@ -286,9 +280,7 @@ api.likeChat = {
|
|||||||
api.flagChat = {
|
api.flagChat = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/chat/:chatId/flag',
|
url: '/groups/:groupId/chat/:chatId/flag',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
const chatReporter = chatReporterFactory('Group', req, res);
|
const chatReporter = chatReporterFactory('Group', req, res);
|
||||||
const message = await chatReporter.flag();
|
const message = await chatReporter.flag();
|
||||||
@@ -317,9 +309,7 @@ api.flagChat = {
|
|||||||
api.clearChatFlags = {
|
api.clearChatFlags = {
|
||||||
method: 'Post',
|
method: 'Post',
|
||||||
url: '/groups/:groupId/chat/:chatId/clearflags',
|
url: '/groups/:groupId/chat/:chatId/clearflags',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let groupId = req.params.groupId;
|
let groupId = req.params.groupId;
|
||||||
@@ -389,9 +379,7 @@ api.clearChatFlags = {
|
|||||||
api.seenChat = {
|
api.seenChat = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/chat/seen',
|
url: '/groups/:groupId/chat/seen',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let groupId = req.params.groupId;
|
let groupId = req.params.groupId;
|
||||||
@@ -457,9 +445,7 @@ api.seenChat = {
|
|||||||
api.deleteChat = {
|
api.deleteChat = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/groups/:groupId/chat/:chatId',
|
url: '/groups/:groupId/chat/:chatId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let groupId = req.params.groupId;
|
let groupId = req.params.groupId;
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import {
|
|||||||
authWithSession,
|
authWithSession,
|
||||||
} from '../../middlewares/auth';
|
} from '../../middlewares/auth';
|
||||||
import { ensureSudo } from '../../middlewares/ensureAccessRight';
|
import { ensureSudo } from '../../middlewares/ensureAccessRight';
|
||||||
import { model as Coupon } from '../../models/coupon';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import * as couponsLib from '../../libs/coupons';
|
||||||
import couponCode from 'coupon-code';
|
import couponCode from 'coupon-code';
|
||||||
import apiError from '../../libs/apiError';
|
import apiError from '../../libs/apiError';
|
||||||
|
import { model as Coupon } from '../../models/coupon';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
@@ -68,9 +69,7 @@ api.getCoupons = {
|
|||||||
api.generateCoupons = {
|
api.generateCoupons = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/coupons/generate/:event',
|
url: '/coupons/generate/:event',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders(), ensureSudo],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
}), ensureSudo],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('event', apiError('eventRequired')).notEmpty();
|
req.checkParams('event', apiError('eventRequired')).notEmpty();
|
||||||
req.checkQuery('count', apiError('countRequired')).notEmpty().isNumeric();
|
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
|
* @api {post} /api/v3/coupons/enter/:code Redeem a coupon code
|
||||||
* @apiName RedeemCouponCode
|
* @apiName RedeemCouponCode
|
||||||
@@ -95,19 +96,12 @@ api.generateCoupons = {
|
|||||||
api.enterCouponCode = {
|
api.enterCouponCode = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/coupons/enter/:code',
|
url: '/coupons/enter/:code',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
const user = res.locals.user;
|
||||||
|
await couponsLib.enterCode(req, res, user);
|
||||||
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
|
const userToJSON = await user.toJSONWithInbox();
|
||||||
|
res.respond(200, userToJSON);
|
||||||
let validationErrors = req.validationErrors();
|
|
||||||
if (validationErrors) throw validationErrors;
|
|
||||||
|
|
||||||
await Coupon.apply(user, req, req.params.code);
|
|
||||||
res.respond(200, user);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,7 +119,6 @@ api.validateCoupon = {
|
|||||||
url: '/coupons/validate/:code',
|
url: '/coupons/validate/:code',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders({
|
||||||
optional: true,
|
optional: true,
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
})],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
|
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
|
||||||
|
|||||||
@@ -109,9 +109,7 @@ let api = {};
|
|||||||
api.createGroup = {
|
api.createGroup = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups',
|
url: '/groups',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let group = new Group(Group.sanitize(req.body));
|
let group = new Group(Group.sanitize(req.body));
|
||||||
@@ -182,9 +180,7 @@ api.createGroup = {
|
|||||||
api.createGroupPlan = {
|
api.createGroupPlan = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/create-plan',
|
url: '/groups/create-plan',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let group = new Group(Group.sanitize(req.body.groupToCreate));
|
let group = new Group(Group.sanitize(req.body.groupToCreate));
|
||||||
@@ -293,9 +289,7 @@ api.createGroupPlan = {
|
|||||||
api.getGroups = {
|
api.getGroups = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/groups',
|
url: '/groups',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -443,9 +437,7 @@ api.getGroup = {
|
|||||||
api.updateGroup = {
|
api.updateGroup = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: '/groups/:groupId',
|
url: '/groups/:groupId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -508,9 +500,7 @@ api.updateGroup = {
|
|||||||
api.joinGroup = {
|
api.joinGroup = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/join',
|
url: '/groups/:groupId/join',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let inviter;
|
let inviter;
|
||||||
@@ -682,9 +672,7 @@ api.joinGroup = {
|
|||||||
api.rejectGroupInvite = {
|
api.rejectGroupInvite = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/reject-invite',
|
url: '/groups/:groupId/reject-invite',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -759,9 +747,7 @@ function _removeMessagesFromMember (member, groupId) {
|
|||||||
api.leaveGroup = {
|
api.leaveGroup = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/leave',
|
url: '/groups/:groupId/leave',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||||
@@ -848,9 +834,7 @@ function _sendMessageToRemoved (group, removedUser, message, isInGroup) {
|
|||||||
api.removeGroupMember = {
|
api.removeGroupMember = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/removeMember/:memberId',
|
url: '/groups/:groupId/removeMember/:memberId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -1176,7 +1160,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
|
|||||||
api.inviteToGroup = {
|
api.inviteToGroup = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/invite',
|
url: '/groups/:groupId/invite',
|
||||||
middlewares: [authWithHeaders({})],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -1249,9 +1233,7 @@ api.inviteToGroup = {
|
|||||||
api.addGroupManager = {
|
api.addGroupManager = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/add-manager',
|
url: '/groups/:groupId/add-manager',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let managerId = req.body.managerId;
|
let managerId = req.body.managerId;
|
||||||
@@ -1300,9 +1282,7 @@ api.addGroupManager = {
|
|||||||
api.removeGroupManager = {
|
api.removeGroupManager = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/remove-manager',
|
url: '/groups/:groupId/remove-manager',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let managerId = req.body.managerId;
|
let managerId = req.body.managerId;
|
||||||
@@ -1355,9 +1335,7 @@ api.removeGroupManager = {
|
|||||||
api.getGroupPlans = {
|
api.getGroupPlans = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/group-plans',
|
url: '/group-plans',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ let api = {};
|
|||||||
api.getPatrons = {
|
api.getPatrons = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/hall/patrons',
|
url: '/hall/patrons',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkQuery('page').optional().isInt({min: 0}, apiError('queryPageInteger'));
|
req.checkQuery('page').optional().isInt({min: 0}, apiError('queryPageInteger'));
|
||||||
|
|
||||||
@@ -123,9 +121,7 @@ api.getPatrons = {
|
|||||||
api.getHeroes = {
|
api.getHeroes = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/hall/heroes',
|
url: '/hall/heroes',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let heroes = await User
|
let heroes = await User
|
||||||
.find({
|
.find({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { authWithHeaders } from '../../middlewares/auth';
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
import { toArray, orderBy } from 'lodash';
|
import * as inboxLib from '../../libs/inbox';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
@@ -7,7 +7,6 @@ let api = {};
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /api/v3/inbox/messages Get inbox messages for a user
|
* @api {get} /api/v3/inbox/messages Get inbox messages for a user
|
||||||
* @apiPrivate
|
|
||||||
* @apiName GetInboxMessages
|
* @apiName GetInboxMessages
|
||||||
* @apiGroup Inbox
|
* @apiGroup Inbox
|
||||||
* @apiDescription Get inbox messages for a user
|
* @apiDescription Get inbox messages for a user
|
||||||
@@ -19,10 +18,11 @@ api.getInboxMessages = {
|
|||||||
url: '/inbox/messages',
|
url: '/inbox/messages',
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
const messagesObj = res.locals.user.inbox.messages;
|
const user = res.locals.user;
|
||||||
const messagesArray = orderBy(toArray(messagesObj), ['timestamp'], ['desc']);
|
|
||||||
|
|
||||||
res.respond(200, messagesArray);
|
const userInbox = await inboxLib.getUserInbox(user);
|
||||||
|
|
||||||
|
res.respond(200, userInbox);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -379,9 +379,7 @@ function _getMembersForItem (type) {
|
|||||||
api.getMembersForGroup = {
|
api.getMembersForGroup = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/groups/:groupId/members',
|
url: '/groups/:groupId/members',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
handler: _getMembersForItem('group-members'),
|
handler: _getMembersForItem('group-members'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -417,9 +415,7 @@ api.getMembersForGroup = {
|
|||||||
api.getInvitesForGroup = {
|
api.getInvitesForGroup = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/groups/:groupId/invites',
|
url: '/groups/:groupId/invites',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
handler: _getMembersForItem('group-invites'),
|
handler: _getMembersForItem('group-invites'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -445,9 +441,7 @@ api.getInvitesForGroup = {
|
|||||||
api.getMembersForChallenge = {
|
api.getMembersForChallenge = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/challenges/:challengeId/members',
|
url: '/challenges/:challengeId/members',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
handler: _getMembersForItem('challenge-members'),
|
handler: _getMembersForItem('challenge-members'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -509,9 +503,7 @@ api.getMembersForChallenge = {
|
|||||||
api.getChallengeMemberProgress = {
|
api.getChallengeMemberProgress = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/challenges/:challengeId/members/:memberId',
|
url: '/challenges/:challengeId/members/:memberId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||||
@@ -594,7 +586,7 @@ api.getObjectionsToInteraction = {
|
|||||||
* @apiParam (Body) {String} message Body parameter - The message
|
* @apiParam (Body) {String} message Body parameter - The message
|
||||||
* @apiParam (Body) {UUID} toUserId Body parameter - The user to contact
|
* @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
|
* @apiUse UserNotFound
|
||||||
*/
|
*/
|
||||||
@@ -617,7 +609,7 @@ api.sendPrivateMessage = {
|
|||||||
const 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]));
|
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) {
|
if (receiver.preferences.emailNotifications.newPM !== false) {
|
||||||
sendTxnEmail(receiver, 'new-pm', [
|
sendTxnEmail(receiver, 'new-pm', [
|
||||||
@@ -638,7 +630,7 @@ api.sendPrivateMessage = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.respond(200, { message: newMessage });
|
res.respond(200, {message: messageSent});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -682,6 +674,7 @@ api.transferGems = {
|
|||||||
|
|
||||||
receiver.balance += amount;
|
receiver.balance += amount;
|
||||||
sender.balance -= amount;
|
sender.balance -= amount;
|
||||||
|
// @TODO necessary? Also saved when sending the inbox message
|
||||||
let promises = [receiver.save(), sender.save()];
|
let promises = [receiver.save(), sender.save()];
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ api.getNews = {
|
|||||||
*/
|
*/
|
||||||
api.tellMeLaterNews = {
|
api.tellMeLaterNews = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/news/tell-me-later',
|
url: '/news/tell-me-later',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
const user = res.locals.user;
|
const user = res.locals.user;
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ let api = {};
|
|||||||
api.readNotification = {
|
api.readNotification = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/notifications/:notificationId/read',
|
url: '/notifications/:notificationId/read',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -67,9 +65,7 @@ api.readNotification = {
|
|||||||
api.readNotifications = {
|
api.readNotifications = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/notifications/read',
|
url: '/notifications/read',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -117,9 +113,7 @@ api.readNotifications = {
|
|||||||
api.seeNotification = {
|
api.seeNotification = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/notifications/:notificationId/see',
|
url: '/notifications/:notificationId/see',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -168,9 +162,7 @@ api.seeNotification = {
|
|||||||
api.seeNotifications = {
|
api.seeNotifications = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/notifications/see',
|
url: '/notifications/see',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ let api = {};
|
|||||||
api.addPushDevice = {
|
api.addPushDevice = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/user/push-devices',
|
url: '/user/push-devices',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
const user = res.locals.user;
|
const user = res.locals.user;
|
||||||
|
|
||||||
@@ -72,9 +70,7 @@ api.addPushDevice = {
|
|||||||
api.removePushDevice = {
|
api.removePushDevice = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/user/push-devices/:regId',
|
url: '/user/push-devices/:regId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
const user = res.locals.user;
|
const user = res.locals.user;
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ let api = {};
|
|||||||
api.inviteToQuest = {
|
api.inviteToQuest = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/quests/invite/:questKey',
|
url: '/groups/:groupId/quests/invite/:questKey',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let questKey = req.params.questKey;
|
let questKey = req.params.questKey;
|
||||||
@@ -171,9 +169,7 @@ api.inviteToQuest = {
|
|||||||
api.acceptQuest = {
|
api.acceptQuest = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/quests/accept',
|
url: '/groups/:groupId/quests/accept',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -232,9 +228,7 @@ api.acceptQuest = {
|
|||||||
api.rejectQuest = {
|
api.rejectQuest = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/quests/reject',
|
url: '/groups/:groupId/quests/reject',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -297,9 +291,7 @@ api.rejectQuest = {
|
|||||||
api.forceStart = {
|
api.forceStart = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/quests/force-start',
|
url: '/groups/:groupId/quests/force-start',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -357,9 +349,7 @@ api.forceStart = {
|
|||||||
api.cancelQuest = {
|
api.cancelQuest = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/quests/cancel',
|
url: '/groups/:groupId/quests/cancel',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
// Cancel a quest BEFORE it has begun (i.e., in the invitation stage)
|
// 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.
|
// Quest scroll has not yet left quest owner's inventory so no need to return it.
|
||||||
@@ -413,9 +403,7 @@ api.cancelQuest = {
|
|||||||
api.abortQuest = {
|
api.abortQuest = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/quests/abort',
|
url: '/groups/:groupId/quests/abort',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
// Abort a quest AFTER it has begun (see questCancel for BEFORE)
|
// Abort a quest AFTER it has begun (see questCancel for BEFORE)
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -475,9 +463,7 @@ api.abortQuest = {
|
|||||||
api.leaveQuest = {
|
api.leaveQuest = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/groups/:groupId/quests/leave',
|
url: '/groups/:groupId/quests/leave',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let groupId = req.params.groupId;
|
let groupId = req.params.groupId;
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ let api = {};
|
|||||||
api.getMarketItems = {
|
api.getMarketItems = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/shops/market',
|
url: '/shops/market',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -38,9 +36,7 @@ api.getMarketItems = {
|
|||||||
api.getMarketGear = {
|
api.getMarketGear = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/shops/market-gear',
|
url: '/shops/market-gear',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -64,9 +60,7 @@ api.getMarketGear = {
|
|||||||
api.getQuestShopItems = {
|
api.getQuestShopItems = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/shops/quests',
|
url: '/shops/quests',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -88,9 +82,7 @@ api.getQuestShopItems = {
|
|||||||
api.getTimeTravelerShopItems = {
|
api.getTimeTravelerShopItems = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/shops/time-travelers',
|
url: '/shops/time-travelers',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -112,9 +104,7 @@ api.getTimeTravelerShopItems = {
|
|||||||
api.getSeasonalShopItems = {
|
api.getSeasonalShopItems = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/shops/seasonal',
|
url: '/shops/seasonal',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -136,9 +126,7 @@ api.getSeasonalShopItems = {
|
|||||||
api.getBackgroundShopItems = {
|
api.getBackgroundShopItems = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/shops/backgrounds',
|
url: '/shops/backgrounds',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ let api = {};
|
|||||||
api.createTag = {
|
api.createTag = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tags',
|
url: '/tags',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -66,9 +64,7 @@ api.createTag = {
|
|||||||
api.getTags = {
|
api.getTags = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tags',
|
url: '/tags',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
res.respond(200, user.tags);
|
res.respond(200, user.tags);
|
||||||
@@ -93,9 +89,7 @@ api.getTags = {
|
|||||||
api.getTag = {
|
api.getTag = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tags/:tagId',
|
url: '/tags/:tagId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -132,9 +126,7 @@ api.getTag = {
|
|||||||
api.updateTag = {
|
api.updateTag = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: '/tags/:tagId',
|
url: '/tags/:tagId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -176,9 +168,7 @@ api.updateTag = {
|
|||||||
api.reorderTags = {
|
api.reorderTags = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/reorder-tags',
|
url: '/reorder-tags',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -217,9 +207,7 @@ api.reorderTags = {
|
|||||||
api.deleteTag = {
|
api.deleteTag = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/tags/:tagId',
|
url: '/tags/:tagId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
|||||||
@@ -158,9 +158,7 @@ let requiredGroupFields = '_id leader tasksOrder name';
|
|||||||
api.createUserTasks = {
|
api.createUserTasks = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/user',
|
url: '/tasks/user',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let tasks = await createTasks(req, res, {user});
|
let tasks = await createTasks(req, res, {user});
|
||||||
@@ -232,9 +230,7 @@ api.createUserTasks = {
|
|||||||
api.createChallengeTasks = {
|
api.createChallengeTasks = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/challenge/:challengeId',
|
url: '/tasks/challenge/:challengeId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
@@ -328,9 +324,7 @@ api.getUserTasks = {
|
|||||||
api.getChallengeTasks = {
|
api.getChallengeTasks = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tasks/challenge/:challengeId',
|
url: '/tasks/challenge/:challengeId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||||
@@ -380,9 +374,7 @@ api.getChallengeTasks = {
|
|||||||
api.getTask = {
|
api.getTask = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tasks/:taskId',
|
url: '/tasks/:taskId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let taskId = req.params.taskId;
|
let taskId = req.params.taskId;
|
||||||
@@ -436,9 +428,7 @@ api.getTask = {
|
|||||||
api.updateTask = {
|
api.updateTask = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: '/tasks/:taskId',
|
url: '/tasks/:taskId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let challenge;
|
let challenge;
|
||||||
@@ -552,9 +542,7 @@ api.updateTask = {
|
|||||||
api.scoreTask = {
|
api.scoreTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/score/:direction',
|
url: '/tasks/:taskId/score/:direction',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('direction', res.t('directionUpDown')).notEmpty().isIn(['up', 'down']);
|
req.checkParams('direction', res.t('directionUpDown')).notEmpty().isIn(['up', 'down']);
|
||||||
|
|
||||||
@@ -728,9 +716,7 @@ api.scoreTask = {
|
|||||||
api.moveTask = {
|
api.moveTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/move/to/:position',
|
url: '/tasks/:taskId/move/to/:position',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||||
@@ -799,9 +785,7 @@ api.moveTask = {
|
|||||||
api.addChecklistItem = {
|
api.addChecklistItem = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/checklist',
|
url: '/tasks/:taskId/checklist',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let challenge;
|
let challenge;
|
||||||
@@ -861,9 +845,7 @@ api.addChecklistItem = {
|
|||||||
api.scoreCheckListItem = {
|
api.scoreCheckListItem = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/checklist/:itemId/score',
|
url: '/tasks/:taskId/checklist/:itemId/score',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -917,9 +899,7 @@ api.scoreCheckListItem = {
|
|||||||
api.updateChecklistItem = {
|
api.updateChecklistItem = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: '/tasks/:taskId/checklist/:itemId',
|
url: '/tasks/:taskId/checklist/:itemId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let challenge;
|
let challenge;
|
||||||
@@ -984,9 +964,7 @@ api.updateChecklistItem = {
|
|||||||
api.removeChecklistItem = {
|
api.removeChecklistItem = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/tasks/:taskId/checklist/:itemId',
|
url: '/tasks/:taskId/checklist/:itemId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let challenge;
|
let challenge;
|
||||||
@@ -1049,9 +1027,7 @@ api.removeChecklistItem = {
|
|||||||
api.addTagToTask = {
|
api.addTagToTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/tags/:tagId',
|
url: '/tasks/:taskId/tags/:tagId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -1100,9 +1076,7 @@ api.addTagToTask = {
|
|||||||
api.removeTagFromTask = {
|
api.removeTagFromTask = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/tasks/:taskId/tags/:tagId',
|
url: '/tasks/:taskId/tags/:tagId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -1147,9 +1121,7 @@ api.removeTagFromTask = {
|
|||||||
api.unlinkAllTasks = {
|
api.unlinkAllTasks = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/unlink-all/:challengeId',
|
url: '/tasks/unlink-all/:challengeId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
req.checkQuery('keep', apiError('keepOrRemoveAll')).notEmpty().isIn(['keep-all', 'remove-all']);
|
req.checkQuery('keep', apiError('keepOrRemoveAll')).notEmpty().isIn(['keep-all', 'remove-all']);
|
||||||
@@ -1216,9 +1188,7 @@ api.unlinkAllTasks = {
|
|||||||
api.unlinkOneTask = {
|
api.unlinkOneTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/unlink-one/:taskId',
|
url: '/tasks/unlink-one/:taskId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||||
req.checkQuery('keep', apiError('keepOrRemove')).notEmpty().isIn(['keep', 'remove']);
|
req.checkQuery('keep', apiError('keepOrRemove')).notEmpty().isIn(['keep', 'remove']);
|
||||||
@@ -1268,9 +1238,7 @@ api.unlinkOneTask = {
|
|||||||
api.clearCompletedTodos = {
|
api.clearCompletedTodos = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/clearCompletedTodos',
|
url: '/tasks/clearCompletedTodos',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
@@ -1321,9 +1289,7 @@ api.clearCompletedTodos = {
|
|||||||
api.deleteTask = {
|
api.deleteTask = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: '/tasks/:taskId',
|
url: '/tasks/:taskId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let challenge;
|
let challenge;
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ let api = {};
|
|||||||
api.createGroupTasks = {
|
api.createGroupTasks = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/group/:groupId',
|
url: '/tasks/group/:groupId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
@@ -88,9 +86,7 @@ api.createGroupTasks = {
|
|||||||
api.getGroupTasks = {
|
api.getGroupTasks = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tasks/group/:groupId',
|
url: '/tasks/group/:groupId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||||
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);
|
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);
|
||||||
@@ -123,9 +119,7 @@ api.getGroupTasks = {
|
|||||||
api.groupMoveTask = {
|
api.groupMoveTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/group-tasks/:taskId/move/to/:position',
|
url: '/group-tasks/:taskId/move/to/:position',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||||
@@ -176,9 +170,7 @@ api.groupMoveTask = {
|
|||||||
api.assignTask = {
|
api.assignTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/assign/:assignedUserId',
|
url: '/tasks/:taskId/assign/:assignedUserId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||||
@@ -238,9 +230,7 @@ api.assignTask = {
|
|||||||
api.unassignTask = {
|
api.unassignTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/unassign/:assignedUserId',
|
url: '/tasks/:taskId/unassign/:assignedUserId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||||
@@ -290,9 +280,7 @@ api.unassignTask = {
|
|||||||
api.approveTask = {
|
api.approveTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/approve/:userId',
|
url: '/tasks/:taskId/approve/:userId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||||
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||||
@@ -390,9 +378,7 @@ api.approveTask = {
|
|||||||
api.taskNeedsWork = {
|
api.taskNeedsWork = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/needs-work/:userId',
|
url: '/tasks/:taskId/needs-work/:userId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||||
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||||
@@ -489,9 +475,7 @@ api.taskNeedsWork = {
|
|||||||
api.getGroupApprovals = {
|
api.getGroupApprovals = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/approvals/group/:groupId',
|
url: '/approvals/group/:groupId',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import {
|
|||||||
basicFields as basicGroupFields,
|
basicFields as basicGroupFields,
|
||||||
model as Group,
|
model as Group,
|
||||||
} from '../../models/group';
|
} from '../../models/group';
|
||||||
import {
|
|
||||||
model as User,
|
|
||||||
} from '../../models/user';
|
|
||||||
import * as Tasks from '../../models/task';
|
import * as Tasks from '../../models/task';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import * as passwordUtils from '../../libs/password';
|
import * as passwordUtils from '../../libs/password';
|
||||||
@@ -22,6 +19,8 @@ import {
|
|||||||
sendTxn as txnEmail,
|
sendTxn as txnEmail,
|
||||||
} from '../../libs/email';
|
} from '../../libs/email';
|
||||||
import Queue from '../../libs/queue';
|
import Queue from '../../libs/queue';
|
||||||
|
import * as inboxLib from '../../libs/inbox';
|
||||||
|
import * as userLib from '../../libs/user';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
@@ -35,6 +34,8 @@ const DELETE_CONFIRMATION = 'DELETE';
|
|||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v4 version */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /api/v3/user Get the authenticated user's profile
|
* @api {get} /api/v3/user Get the authenticated user's profile
|
||||||
* @apiName UserGet
|
* @apiName UserGet
|
||||||
@@ -47,7 +48,7 @@ let api = {};
|
|||||||
* Flags (including armoire, tutorial, tour etc...)
|
* Flags (including armoire, tutorial, tour etc...)
|
||||||
* Guilds
|
* Guilds
|
||||||
* History (including timestamps and values)
|
* History (including timestamps and values)
|
||||||
* Inbox (includes message history)
|
* Inbox
|
||||||
* Invitations (to parties/guilds)
|
* Invitations (to parties/guilds)
|
||||||
* Items (character's full inventory)
|
* Items (character's full inventory)
|
||||||
* New Messages (flags for groups/guilds that have new messages)
|
* New Messages (flags for groups/guilds that have new messages)
|
||||||
@@ -83,20 +84,7 @@ api.getUser = {
|
|||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
url: '/user',
|
url: '/user',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
await userLib.get(req, res, { isV3: true });
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,9 +116,7 @@ api.getUser = {
|
|||||||
*/
|
*/
|
||||||
api.getBuyList = {
|
api.getBuyList = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/inventory/buy',
|
url: '/user/inventory/buy',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let list = _.cloneDeep(common.updateStore(res.locals.user));
|
let list = _.cloneDeep(common.updateStore(res.locals.user));
|
||||||
@@ -173,9 +159,7 @@ api.getBuyList = {
|
|||||||
*/
|
*/
|
||||||
api.getInAppRewardsList = {
|
api.getInAppRewardsList = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/in-app-rewards',
|
url: '/user/in-app-rewards',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let list = common.inAppRewards(res.locals.user);
|
let list = common.inAppRewards(res.locals.user);
|
||||||
@@ -191,78 +175,7 @@ api.getInAppRewardsList = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let updatablePaths = [
|
/* NOTE this route has also an API v4 version */
|
||||||
'_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);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {put} /api/v3/user Update the user
|
* @api {put} /api/v3/user Update the user
|
||||||
@@ -297,67 +210,7 @@ api.updateUser = {
|
|||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
url: '/user',
|
url: '/user',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
await userLib.update(req, res, { isV3: true });
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -488,7 +341,7 @@ api.getUserAnonymized = {
|
|||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
url: '/user/anonymized',
|
url: '/user/anonymized',
|
||||||
async handler (req, res) {
|
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.toNextLevel = common.tnl(user.stats.lvl);
|
||||||
user.stats.maxHealth = common.maxHealth;
|
user.stats.maxHealth = common.maxHealth;
|
||||||
user.stats.maxMP = common.statsComputed(res.locals.user).maxMP;
|
user.stats.maxMP = common.statsComputed(res.locals.user).maxMP;
|
||||||
@@ -513,6 +366,7 @@ api.getUserAnonymized = {
|
|||||||
_.forEach(user.inbox.messages, (msg) => {
|
_.forEach(user.inbox.messages, (msg) => {
|
||||||
msg.text = 'inbox message text';
|
msg.text = 'inbox message text';
|
||||||
});
|
});
|
||||||
|
|
||||||
_.forEach(user.tags, (tag) => {
|
_.forEach(user.tags, (tag) => {
|
||||||
tag.name = 'tag';
|
tag.name = 'tag';
|
||||||
tag.challenge = 'challenge';
|
tag.challenge = 'challenge';
|
||||||
@@ -556,9 +410,7 @@ api.getUserAnonymized = {
|
|||||||
*/
|
*/
|
||||||
api.sleep = {
|
api.sleep = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/sleep',
|
url: '/user/sleep',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -602,9 +454,7 @@ const buyKnownKeys = ['armoire', 'mystery', 'potion', 'quest', 'special'];
|
|||||||
*/
|
*/
|
||||||
api.buy = {
|
api.buy = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/buy/:key',
|
url: '/user/buy/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -668,9 +518,7 @@ api.buy = {
|
|||||||
*/
|
*/
|
||||||
api.buyGear = {
|
api.buyGear = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/buy-gear/:key',
|
url: '/user/buy-gear/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -710,9 +558,7 @@ api.buyGear = {
|
|||||||
*/
|
*/
|
||||||
api.buyArmoire = {
|
api.buyArmoire = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/buy-armoire',
|
url: '/user/buy-armoire',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -752,9 +598,7 @@ api.buyArmoire = {
|
|||||||
*/
|
*/
|
||||||
api.buyHealthPotion = {
|
api.buyHealthPotion = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/buy-health-potion',
|
url: '/user/buy-health-potion',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -796,9 +640,7 @@ api.buyHealthPotion = {
|
|||||||
*/
|
*/
|
||||||
api.buyMysterySet = {
|
api.buyMysterySet = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/buy-mystery-set/:key',
|
url: '/user/buy-mystery-set/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -841,9 +683,7 @@ api.buyMysterySet = {
|
|||||||
*/
|
*/
|
||||||
api.buyQuest = {
|
api.buyQuest = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/buy-quest/:key',
|
url: '/user/buy-quest/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -883,9 +723,7 @@ api.buyQuest = {
|
|||||||
*/
|
*/
|
||||||
api.buySpecialSpell = {
|
api.buySpecialSpell = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/buy-special-spell/:key',
|
url: '/user/buy-special-spell/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -929,9 +767,7 @@ api.buySpecialSpell = {
|
|||||||
*/
|
*/
|
||||||
api.hatch = {
|
api.hatch = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/hatch/:egg/:hatchingPotion',
|
url: '/user/hatch/:egg/:hatchingPotion',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -983,9 +819,7 @@ api.hatch = {
|
|||||||
*/
|
*/
|
||||||
api.equip = {
|
api.equip = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/equip/:type/:key',
|
url: '/user/equip/:type/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1020,9 +854,7 @@ api.equip = {
|
|||||||
*/
|
*/
|
||||||
api.feed = {
|
api.feed = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/feed/:pet/:food',
|
url: '/user/feed/:pet/:food',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1066,9 +898,7 @@ api.feed = {
|
|||||||
*/
|
*/
|
||||||
api.changeClass = {
|
api.changeClass = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/change-class',
|
url: '/user/change-class',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1089,9 +919,7 @@ api.changeClass = {
|
|||||||
*/
|
*/
|
||||||
api.disableClasses = {
|
api.disableClasses = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/disable-classes',
|
url: '/user/disable-classes',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1123,9 +951,7 @@ api.disableClasses = {
|
|||||||
*/
|
*/
|
||||||
api.purchase = {
|
api.purchase = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/purchase/:type/:key',
|
url: '/user/purchase/:type/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1172,9 +998,7 @@ api.purchase = {
|
|||||||
*/
|
*/
|
||||||
api.userPurchaseHourglass = {
|
api.userPurchaseHourglass = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/purchase-hourglass/:type/:key',
|
url: '/user/purchase-hourglass/:type/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1226,9 +1050,7 @@ api.userPurchaseHourglass = {
|
|||||||
*/
|
*/
|
||||||
api.readCard = {
|
api.readCard = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/read-card/:cardType',
|
url: '/user/read-card/:cardType',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1270,9 +1092,7 @@ api.readCard = {
|
|||||||
*/
|
*/
|
||||||
api.userOpenMysteryItem = {
|
api.userOpenMysteryItem = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/open-mystery-item',
|
url: '/user/open-mystery-item',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1304,9 +1124,7 @@ api.userOpenMysteryItem = {
|
|||||||
*/
|
*/
|
||||||
api.userReleasePets = {
|
api.userReleasePets = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/release-pets',
|
url: '/user/release-pets',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1355,9 +1173,7 @@ api.userReleasePets = {
|
|||||||
*/
|
*/
|
||||||
api.userReleaseBoth = {
|
api.userReleaseBoth = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/release-both',
|
url: '/user/release-both',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1393,9 +1209,7 @@ api.userReleaseBoth = {
|
|||||||
*/
|
*/
|
||||||
api.userReleaseMounts = {
|
api.userReleaseMounts = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/release-mounts',
|
url: '/user/release-mounts',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1425,9 +1239,7 @@ api.userReleaseMounts = {
|
|||||||
*/
|
*/
|
||||||
api.userSell = {
|
api.userSell = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/sell/:type/:key',
|
url: '/user/sell/:type/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1470,9 +1282,7 @@ api.userSell = {
|
|||||||
*/
|
*/
|
||||||
api.userUnlock = {
|
api.userUnlock = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/unlock',
|
url: '/user/unlock',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1498,9 +1308,7 @@ api.userUnlock = {
|
|||||||
*/
|
*/
|
||||||
api.userRevive = {
|
api.userRevive = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/revive',
|
url: '/user/revive',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
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
|
* @api {post} /api/v3/user/rebirth Use Orb of Rebirth on user
|
||||||
* @apiName UserRebirth
|
* @apiName UserRebirth
|
||||||
@@ -1540,27 +1350,10 @@ api.userRevive = {
|
|||||||
*/
|
*/
|
||||||
api.userRebirth = {
|
api.userRebirth = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/rebirth',
|
url: '/user/rebirth',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
await userLib.rebirth(req, res, { isV3: true });
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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
|
* @api {delete} /api/v3/user/messages/:id Delete a message
|
||||||
* @apiName deleteMessage
|
* @apiName deleteMessage
|
||||||
@@ -1625,12 +1420,15 @@ api.deleteMessage = {
|
|||||||
url: '/user/messages/:id',
|
url: '/user/messages/:id',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let deletePMRes = common.ops.deletePM(user, req);
|
|
||||||
await user.save();
|
await inboxLib.deleteMessage(user, req.params.id);
|
||||||
res.respond(200, ...deletePMRes);
|
|
||||||
|
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
|
* @api {delete} /api/v3/user/messages Delete all messages
|
||||||
* @apiName clearMessages
|
* @apiName clearMessages
|
||||||
@@ -1647,9 +1445,10 @@ api.clearMessages = {
|
|||||||
url: '/user/messages',
|
url: '/user/messages',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let clearPMsRes = common.ops.clearPMs(user, req);
|
|
||||||
await user.save();
|
await inboxLib.clearPMs(user);
|
||||||
res.respond(200, ...clearPMsRes);
|
|
||||||
|
res.respond(200, ...[]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1658,7 +1457,7 @@ api.clearMessages = {
|
|||||||
* @apiName markPmsRead
|
* @apiName markPmsRead
|
||||||
* @apiGroup User
|
* @apiGroup User
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object} data user.inbox.messages
|
* @apiSuccess {Object} data user.inbox.newMessages
|
||||||
*
|
*
|
||||||
* @apiSuccessExample {json}
|
* @apiSuccessExample {json}
|
||||||
* {"success":true,"data":[0,"Your private messages have been marked as read"],"notifications":[]}
|
* {"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
|
* @api {post} /api/v3/user/reroll Reroll a user using the Fortify Potion
|
||||||
* @apiName UserReroll
|
* @apiName UserReroll
|
||||||
@@ -1700,29 +1501,15 @@ api.markPmsRead = {
|
|||||||
*/
|
*/
|
||||||
api.userReroll = {
|
api.userReroll = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/reroll',
|
url: '/user/reroll',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
await userLib.reroll(req, res, { isV3: true });
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v4 version */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/user/reset Reset user
|
* @api {post} /api/v3/user/reset Reset user
|
||||||
* @apiName UserReset
|
* @apiName UserReset
|
||||||
@@ -1746,32 +1533,10 @@ api.userReroll = {
|
|||||||
*/
|
*/
|
||||||
api.userReset = {
|
api.userReset = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/reset',
|
url: '/user/reset',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
await userLib.reset(req, res, { isV3: true });
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1799,9 +1564,7 @@ api.userReset = {
|
|||||||
*/
|
*/
|
||||||
api.setCustomDayStart = {
|
api.setCustomDayStart = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/custom-day-start',
|
url: '/user/custom-day-start',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1839,9 +1602,7 @@ api.setCustomDayStart = {
|
|||||||
*/
|
*/
|
||||||
api.togglePinnedItem = {
|
api.togglePinnedItem = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/toggle-pinned-item/:type/:path',
|
url: '/user/toggle-pinned-item/:type/:path',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -1879,9 +1640,7 @@ api.togglePinnedItem = {
|
|||||||
api.movePinnedItem = {
|
api.movePinnedItem = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/user/move-pinned-item/:path/move/to/:position',
|
url: '/user/move-pinned-item/:path/move/to/:position',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('path', res.t('taskIdRequired')).notEmpty();
|
req.checkParams('path', res.t('taskIdRequired')).notEmpty();
|
||||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||||
|
|||||||
@@ -1,25 +1,12 @@
|
|||||||
import { authWithHeaders } from '../../../middlewares/auth';
|
import { authWithHeaders } from '../../../middlewares/auth';
|
||||||
import common from '../../../../common';
|
|
||||||
import {
|
import {
|
||||||
model as Group,
|
castSpell,
|
||||||
} from '../../../models/group';
|
|
||||||
import {
|
|
||||||
NotAuthorized,
|
|
||||||
NotFound,
|
|
||||||
} from '../../../libs/errors';
|
|
||||||
import {
|
|
||||||
castTaskSpell,
|
|
||||||
castMultiTaskSpell,
|
|
||||||
castSelfSpell,
|
|
||||||
castPartySpell,
|
|
||||||
castUserSpell,
|
|
||||||
} from '../../../libs/spells';
|
} from '../../../libs/spells';
|
||||||
import apiError from '../../../libs/apiError';
|
|
||||||
|
|
||||||
const partyMembersFields = 'profile.name stats achievements items.special';
|
|
||||||
|
|
||||||
let api = {};
|
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
|
* @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target
|
||||||
* @apiName UserCast
|
* @apiName UserCast
|
||||||
@@ -72,69 +59,9 @@ api.castSpell = {
|
|||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
url: '/user/class/cast/:spellId',
|
url: '/user/class/cast/:spellId',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
await castSpell(req, res, {
|
||||||
let spellId = req.params.spellId;
|
isV3: true,
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ let api = {};
|
|||||||
*/
|
*/
|
||||||
api.allocate = {
|
api.allocate = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/allocate',
|
url: '/user/allocate',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -69,9 +67,7 @@ api.allocate = {
|
|||||||
*/
|
*/
|
||||||
api.allocateBulk = {
|
api.allocateBulk = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/allocate-bulk',
|
url: '/user/allocate-bulk',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -128,9 +124,7 @@ api.allocateBulk = {
|
|||||||
*/
|
*/
|
||||||
api.allocateNow = {
|
api.allocateNow = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/allocate-now',
|
url: '/user/allocate-now',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ let api = {};
|
|||||||
*/
|
*/
|
||||||
api.addWebhook = {
|
api.addWebhook = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/webhook',
|
url: '/user/webhook',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -135,9 +133,7 @@ api.addWebhook = {
|
|||||||
*/
|
*/
|
||||||
api.updateWebhook = {
|
api.updateWebhook = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/webhook/:id',
|
url: '/user/webhook/:id',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
@@ -188,9 +184,7 @@ api.updateWebhook = {
|
|||||||
*/
|
*/
|
||||||
api.deleteWebhook = {
|
api.deleteWebhook = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
middlewares: [authWithHeaders({
|
middlewares: [authWithHeaders()],
|
||||||
userFieldsToExclude: ['inbox'],
|
|
||||||
})],
|
|
||||||
url: '/user/webhook/:id',
|
url: '/user/webhook/:id',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|||||||
38
website/server/controllers/api-v4/auth.js
Normal file
38
website/server/controllers/api-v4/auth.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
|
import * as authLib from '../../libs/auth';
|
||||||
|
|
||||||
|
const api = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE most user routes are still in the v3 controller
|
||||||
|
* here there are only routes that had to be split from the v3 version because of
|
||||||
|
* some breaking change (for example because their returned the entire user object).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v4/user/auth/local/register Register
|
||||||
|
* @apiDescription Register a new user with email, login name, and password or attach local auth to a social user
|
||||||
|
* @apiName UserRegisterLocal
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiParam (Body) {String} username Login name of the new user. Must be 1-36 characters, containing only a-z, 0-9, hyphens (-), or underscores (_).
|
||||||
|
* @apiParam (Body) {String} email Email address of the new user
|
||||||
|
* @apiParam (Body) {String} password Password for the new user
|
||||||
|
* @apiParam (Body) {String} confirmPassword Password confirmation
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data The user object, if local auth was just attached to a social user then only user.auth.local
|
||||||
|
*/
|
||||||
|
api.registerLocal = {
|
||||||
|
method: 'POST',
|
||||||
|
middlewares: [authWithHeaders({
|
||||||
|
optional: true,
|
||||||
|
})],
|
||||||
|
url: '/user/auth/local/register',
|
||||||
|
async handler (req, res) {
|
||||||
|
await authLib.registerLocal(req, res, { isV3: false });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
34
website/server/controllers/api-v4/coupon.js
Normal file
34
website/server/controllers/api-v4/coupon.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
|
import * as couponsLib from '../../libs/coupons';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE most coupons routes are still in the v3 controller
|
||||||
|
* here there are only routes that had to be split from the v3 version because of
|
||||||
|
* some breaking change (for example because their returned the entire user object).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const api = {};
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v4/coupons/enter/:code Redeem a coupon code
|
||||||
|
* @apiName RedeemCouponCode
|
||||||
|
* @apiGroup Coupon
|
||||||
|
*
|
||||||
|
* @apiParam (Path) {String} code The coupon code to apply
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data User object
|
||||||
|
*/
|
||||||
|
api.enterCouponCode = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/coupons/enter/:code',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
async handler (req, res) {
|
||||||
|
const user = res.locals.user;
|
||||||
|
await couponsLib.enterCode(req, res, user);
|
||||||
|
res.respond(200, user);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
@@ -47,4 +47,29 @@ api.deleteMessage = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /api/v4/inbox/clear Delete all messages
|
||||||
|
* @apiName clearMessages
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data Empty object
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json}
|
||||||
|
* {"success":true,"data":{},"notifications":[]}
|
||||||
|
*/
|
||||||
|
api.clearMessages = {
|
||||||
|
method: 'DELETE',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
url: '/inbox/clear',
|
||||||
|
async handler (req, res) {
|
||||||
|
const user = res.locals.user;
|
||||||
|
|
||||||
|
await inboxLib.clearPMs(user);
|
||||||
|
|
||||||
|
res.respond(200, {});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = api;
|
module.exports = api;
|
||||||
|
|||||||
209
website/server/controllers/api-v4/user.js
Normal file
209
website/server/controllers/api-v4/user.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
|
import * as userLib from '../../libs/user';
|
||||||
|
|
||||||
|
const api = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE most user routes are still in the v3 controller
|
||||||
|
* here there are only routes that had to be split from the v3 version because of
|
||||||
|
* some breaking change (for example because their returned the entire user object).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /api/v4/user Get the authenticated user's profile
|
||||||
|
* @apiName UserGet
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiDescription The user profile contains data related to the authenticated user including (but not limited to);
|
||||||
|
* Achievements
|
||||||
|
* Authentications (including types and timestamps)
|
||||||
|
* Challenges
|
||||||
|
* Flags (including armoire, tutorial, tour etc...)
|
||||||
|
* Guilds
|
||||||
|
* History (including timestamps and values)
|
||||||
|
* Inbox (without messages in v4)
|
||||||
|
* Invitations (to parties/guilds)
|
||||||
|
* Items (character's full inventory)
|
||||||
|
* New Messages (flags for groups/guilds that have new messages)
|
||||||
|
* Notifications
|
||||||
|
* Party (includes current quest information)
|
||||||
|
* Preferences (user selected prefs)
|
||||||
|
* Profile (name, photo url, blurb)
|
||||||
|
* Purchased (includes purchase history, gem purchased items, plans)
|
||||||
|
* PushDevices (identifiers for mobile devices authorized)
|
||||||
|
* Stats (standard RPG stats, class, buffs, xp, etc..)
|
||||||
|
* Tags
|
||||||
|
* TasksOrder (list of all ids for dailys, habits, rewards and todos)
|
||||||
|
*
|
||||||
|
* @apiParam (Query) {UUID} userFields A list of comma separated user fields to be returned instead of the entire document. Notifications are always returned.
|
||||||
|
*
|
||||||
|
* @apiExample {curl} Example use:
|
||||||
|
* curl -i https://habitica.com/api/v3/user?userFields=achievements,items.mounts
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data The user object
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json} Result:
|
||||||
|
* {
|
||||||
|
* "success": true,
|
||||||
|
* "data": {
|
||||||
|
* -- User data included here, for details of the user model see:
|
||||||
|
* -- https://github.com/HabitRPG/habitica/tree/develop/website/server/models/user
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
api.getUser = {
|
||||||
|
method: 'GET',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
url: '/user',
|
||||||
|
async handler (req, res) {
|
||||||
|
await userLib.get(req, res, { isV3: false });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {put} /api/v4/user Update the user
|
||||||
|
* @apiName UserUpdate
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiDescription Some of the user items can be updated, such as preferences, flags and stats.
|
||||||
|
^
|
||||||
|
* @apiParamExample {json} Request-Example:
|
||||||
|
* {
|
||||||
|
* "achievements.habitBirthdays": 2,
|
||||||
|
* "profile.name": "MadPink",
|
||||||
|
* "stats.hp": 53,
|
||||||
|
* "flags.warnedLowHealth":false,
|
||||||
|
* "preferences.allocationMode":"flat",
|
||||||
|
* "preferences.hair.bangs": 3
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data The updated user object, the result is identical to the get user call
|
||||||
|
*
|
||||||
|
* @apiError (401) {NotAuthorized} messageUserOperationProtected Returned if the change is not allowed.
|
||||||
|
*
|
||||||
|
* @apiErrorExample {json} Error-Response:
|
||||||
|
* {
|
||||||
|
* "success": false,
|
||||||
|
* "error": "NotAuthorized",
|
||||||
|
* "message": "path `stats.class` was not saved, as it's a protected path."
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
api.updateUser = {
|
||||||
|
method: 'PUT',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
url: '/user',
|
||||||
|
async handler (req, res) {
|
||||||
|
await userLib.update(req, res, { isV3: false });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v4/user/rebirth Use Orb of Rebirth on user
|
||||||
|
* @apiName UserRebirth
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data.user
|
||||||
|
* @apiSuccess {Array} data.tasks User's modified tasks (no rewards)
|
||||||
|
* @apiSuccess {String} message Success message
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json}
|
||||||
|
* {
|
||||||
|
* "success": true,
|
||||||
|
* "data": {
|
||||||
|
* },
|
||||||
|
* "message": "You have been reborn!"
|
||||||
|
* {
|
||||||
|
* "type": "REBIRTH_ACHIEVEMENT",
|
||||||
|
* "data": {},
|
||||||
|
* "id": "424d69fa-3a6d-47db-96a4-6db42ed77a43"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiError {NotAuthorized} Not enough gems
|
||||||
|
*
|
||||||
|
* @apiErrorExample {json}
|
||||||
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
||||||
|
*/
|
||||||
|
api.userRebirth = {
|
||||||
|
method: 'POST',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
url: '/user/rebirth',
|
||||||
|
async handler (req, res) {
|
||||||
|
await userLib.rebirth(req, res, { isV3: false });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v4/user/reroll Reroll a user using the Fortify Potion
|
||||||
|
* @apiName UserReroll
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data.user
|
||||||
|
* @apiSuccess {Object} data.tasks User's modified tasks (no rewards)
|
||||||
|
* @apiSuccess {Object} message Success message
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json}
|
||||||
|
* {
|
||||||
|
* "success": true,
|
||||||
|
* "data": {
|
||||||
|
* },
|
||||||
|
* "message": "Fortify complete!"
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @apiError {NotAuthorized} Not enough gems
|
||||||
|
*
|
||||||
|
* @apiErrorExample {json}
|
||||||
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
||||||
|
*/
|
||||||
|
api.userReroll = {
|
||||||
|
method: 'POST',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
url: '/user/reroll',
|
||||||
|
async handler (req, res) {
|
||||||
|
await userLib.reroll(req, res, { isV3: false });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v4/user/reset Reset user
|
||||||
|
* @apiName UserReset
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data.user
|
||||||
|
* @apiSuccess {Array} data.tasksToRemove IDs of removed tasks
|
||||||
|
* @apiSuccess {String} message Success message
|
||||||
|
*
|
||||||
|
* @apiSuccessExample {json}
|
||||||
|
* {
|
||||||
|
* "success": true,
|
||||||
|
* "data": {--TRUNCATED--},
|
||||||
|
* "tasksToRemove": [
|
||||||
|
* "ebb8748c-0565-431e-9036-b908da25c6b4",
|
||||||
|
* "12a1cecf-68eb-40a7-b282-4f388c32124c"
|
||||||
|
* ]
|
||||||
|
* },
|
||||||
|
* "message": "Reset complete!"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
api.userReset = {
|
||||||
|
method: 'POST',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
url: '/user/reset',
|
||||||
|
async handler (req, res) {
|
||||||
|
await userLib.reset(req, res, { isV3: false });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
74
website/server/controllers/api-v4/user/spells.js
Normal file
74
website/server/controllers/api-v4/user/spells.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { authWithHeaders } from '../../../middlewares/auth';
|
||||||
|
import {
|
||||||
|
castSpell,
|
||||||
|
} from '../../../libs/spells';
|
||||||
|
|
||||||
|
let api = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* NOTE most spells routes are still in the v3 controller
|
||||||
|
* here there are only routes that had to be split from the v3 version because of
|
||||||
|
* some breaking change (for example because their returned the entire user object).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* NOTE this route has also an API v3 version */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v4/user/class/cast/:spellId Cast a skill (spell) on a target
|
||||||
|
* @apiName UserCast
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
|
||||||
|
* @apiParam (Path) {String=fireball, mpheal, earth, frost, smash, defensiveStance, valorousPresence, intimidate, pickPocket, backStab, toolsOfTrade, stealth, heal, protectAura, brightness, healAll} spellId The skill to cast.
|
||||||
|
* @apiParam (Query) {UUID} targetId Query parameter, necessary if the spell is cast on a party member or task. Not used if the spell is case on the user or the user's current party.
|
||||||
|
* @apiParamExample {json} Query example:
|
||||||
|
* Cast "Pickpocket" on a task:
|
||||||
|
* https://habitica.com/api/v3/user/class/cast/pickPocket?targetId=fd427623...
|
||||||
|
*
|
||||||
|
* Cast "Tools of the Trade" on the party:
|
||||||
|
* https://habitica.com/api/v3/user/class/cast/toolsOfTrade
|
||||||
|
*
|
||||||
|
* @apiSuccess data Will return the modified targets. For party members only the necessary fields will be populated. The user is always returned.
|
||||||
|
*
|
||||||
|
* @apiDescription Skill Key to Name Mapping
|
||||||
|
* Mage
|
||||||
|
* fireball: "Burst of Flames"
|
||||||
|
* mpheal: "Ethereal Surge"
|
||||||
|
* earth: "Earthquake"
|
||||||
|
* frost: "Chilling Frost"
|
||||||
|
*
|
||||||
|
* Warrior
|
||||||
|
* smash: "Brutal Smash"
|
||||||
|
* defensiveStance: "Defensive Stance"
|
||||||
|
* valorousPresence: "Valorous Presence"
|
||||||
|
* intimidate: "Intimidating Gaze"
|
||||||
|
*
|
||||||
|
* Rogue
|
||||||
|
* pickPocket: "Pickpocket"
|
||||||
|
* backStab: "Backstab"
|
||||||
|
* toolsOfTrade: "Tools of the Trade"
|
||||||
|
* stealth: "Stealth"
|
||||||
|
*
|
||||||
|
* Healer
|
||||||
|
* heal: "Healing Light"
|
||||||
|
* protectAura: "Protective Aura"
|
||||||
|
* brightness: "Searing Brightness"
|
||||||
|
* healAll: "Blessing"
|
||||||
|
*
|
||||||
|
* @apiError (400) {NotAuthorized} Not enough mana.
|
||||||
|
* @apiUse TaskNotFound
|
||||||
|
* @apiUse PartyNotFound
|
||||||
|
* @apiUse UserNotFound
|
||||||
|
*/
|
||||||
|
api.castSpell = {
|
||||||
|
method: 'POST',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
url: '/user/class/cast/:spellId',
|
||||||
|
async handler (req, res) {
|
||||||
|
await castSpell(req, res, {
|
||||||
|
isV3: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { authWithSession } from '../../middlewares/auth';
|
import { authWithSession } from '../../middlewares/auth';
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
|
import * as inboxLib from '../../libs/inbox';
|
||||||
import * as Tasks from '../../models/task';
|
import * as Tasks from '../../models/task';
|
||||||
import {
|
import {
|
||||||
NotFound,
|
NotFound,
|
||||||
@@ -81,15 +82,23 @@ api.exportUserHistory = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert user to json and attach tasks divided by type
|
// Convert user to json and attach tasks divided by type and inbox messages
|
||||||
// at user.tasks[`${taskType}s`] (user.tasks.{dailys/habits/...})
|
// at user.tasks[`${taskType}s`] (user.tasks.{dailys/habits/...})
|
||||||
async function _getUserDataForExport (user, xmlMode = false) {
|
async function _getUserDataForExport (user, xmlMode = false) {
|
||||||
let userData = user.toJSON();
|
let userData = user.toJSON();
|
||||||
userData.tasks = {};
|
userData.tasks = {};
|
||||||
|
|
||||||
let tasks = await Tasks.Task.find({
|
userData.inbox.messages = {};
|
||||||
|
|
||||||
|
const [tasks, messages] = await Promise.all([
|
||||||
|
Tasks.Task.find({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
}).exec();
|
}).exec(),
|
||||||
|
|
||||||
|
inboxLib.getUserInbox(user, false),
|
||||||
|
]);
|
||||||
|
|
||||||
|
userData.inbox.messages = messages;
|
||||||
|
|
||||||
_(tasks)
|
_(tasks)
|
||||||
.map(task => task.toJSON())
|
.map(task => task.toJSON())
|
||||||
@@ -296,18 +305,14 @@ api.exportUserPrivateMessages = {
|
|||||||
url: '/export/inbox.html',
|
url: '/export/inbox.html',
|
||||||
middlewares: [authWithSession],
|
middlewares: [authWithSession],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
const user = res.locals.user;
|
||||||
|
|
||||||
const timezoneOffset = user.preferences.timezoneOffset;
|
const timezoneOffset = user.preferences.timezoneOffset;
|
||||||
const dateFormat = user.preferences.dateFormat.toUpperCase();
|
const dateFormat = user.preferences.dateFormat.toUpperCase();
|
||||||
const TO = res.t('to');
|
const TO = res.t('to');
|
||||||
const FROM = res.t('from');
|
const FROM = res.t('from');
|
||||||
|
|
||||||
let inbox = Object.keys(user.inbox.messages).map(key => user.inbox.messages[key]);
|
const inbox = await inboxLib.getUserInbox(user);
|
||||||
|
|
||||||
inbox = _.sortBy(inbox, function sortBy (num) {
|
|
||||||
return num.sort * -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
let messages = '<!DOCTYPE html><html><head></head><body>';
|
let messages = '<!DOCTYPE html><html><head></head><body>';
|
||||||
|
|
||||||
|
|||||||
171
website/server/libs/auth/index.js
Normal file
171
website/server/libs/auth/index.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import {
|
||||||
|
NotAuthorized,
|
||||||
|
NotFound,
|
||||||
|
} from '../../libs/errors';
|
||||||
|
import * as passwordUtils from '../../libs/password';
|
||||||
|
import { model as User } from '../../models/user';
|
||||||
|
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||||
|
import { sendTxn as sendTxnEmail } from '../../libs/email';
|
||||||
|
import common from '../../../common';
|
||||||
|
import logger from '../../libs/logger';
|
||||||
|
import { decrypt } from '../../libs/encryption';
|
||||||
|
import { model as Group } from '../../models/group';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const USERNAME_LENGTH_MIN = 1;
|
||||||
|
const USERNAME_LENGTH_MAX = 20;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerLocal (req, res, { isV3 = false }) {
|
||||||
|
const 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();
|
||||||
|
|
||||||
|
let userToJSON;
|
||||||
|
if (isV3) {
|
||||||
|
userToJSON = await savedUser.toJSONWithInbox();
|
||||||
|
} else {
|
||||||
|
userToJSON = savedUser.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
res.respond(200, userToJSON.auth.local); // We convert to toJSON to hide private fields
|
||||||
|
} else {
|
||||||
|
let userJSON = userToJSON;
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { model as Chat } from '../../models/chat';
|
import { chatModel as Chat } from '../../models/message';
|
||||||
import { MAX_CHAT_COUNT, MAX_SUBBED_GROUP_CHAT_COUNT } from '../../models/group';
|
import { MAX_CHAT_COUNT, MAX_SUBBED_GROUP_CHAT_COUNT } from '../../models/group';
|
||||||
|
|
||||||
// @TODO: Don't use this method when the group can be saved.
|
// @TODO: Don't use this method when the group can be saved.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { getGroupUrl, sendTxn } from '../email';
|
import { getGroupUrl, sendTxn } from '../email';
|
||||||
import slack from '../slack';
|
import slack from '../slack';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
import { model as Chat } from '../../models/chat';
|
import { chatModel as Chat } from '../../models/message';
|
||||||
import apiError from '../apiError';
|
import apiError from '../apiError';
|
||||||
|
|
||||||
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
||||||
|
|||||||
10
website/server/libs/coupons/index.js
Normal file
10
website/server/libs/coupons/index.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { model as Coupon } from '../../models/coupon';
|
||||||
|
|
||||||
|
export async function enterCode (req, res, user) {
|
||||||
|
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
|
||||||
|
|
||||||
|
let validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
await Coupon.apply(user, req, req.params.code);
|
||||||
|
}
|
||||||
@@ -1,11 +1,47 @@
|
|||||||
|
import { inboxModel as Inbox } from '../../models/message';
|
||||||
|
import { toArray, orderBy } from 'lodash';
|
||||||
|
|
||||||
|
export async function getUserInbox (user, asArray = true) {
|
||||||
|
const messages = (await Inbox
|
||||||
|
.find({ownerId: user._id})
|
||||||
|
.exec()).map(msg => msg.toJSON());
|
||||||
|
|
||||||
|
const messagesObj = Object.assign({}, user.inbox.messages); // copy, shallow clone
|
||||||
|
|
||||||
|
if (asArray) {
|
||||||
|
messages.push(...toArray(messagesObj));
|
||||||
|
|
||||||
|
return orderBy(messages, ['timestamp'], ['desc']);
|
||||||
|
} else {
|
||||||
|
messages.forEach(msg => messagesObj[msg._id] = msg);
|
||||||
|
|
||||||
|
return messagesObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteMessage (user, messageId) {
|
export async function deleteMessage (user, messageId) {
|
||||||
if (user.inbox.messages[messageId]) {
|
if (user.inbox.messages[messageId]) { // compatibility
|
||||||
delete user.inbox.messages[messageId];
|
delete user.inbox.messages[messageId];
|
||||||
user.markModified(`inbox.messages.${messageId}`);
|
user.markModified(`inbox.messages.${messageId}`);
|
||||||
await user.save();
|
await user.save();
|
||||||
} else {
|
} else {
|
||||||
return false;
|
const message = await Inbox.findOne({_id: messageId, ownerId: user._id }).exec();
|
||||||
|
if (!message) return false;
|
||||||
|
await Inbox.remove({_id: message._id, ownerId: user._id}).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function clearPMs (user) {
|
||||||
|
user.inbox.newMessages = 0;
|
||||||
|
|
||||||
|
// compatibility
|
||||||
|
user.inbox.messages = {};
|
||||||
|
user.markModified('inbox.messages');
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
user.save(),
|
||||||
|
Inbox.remove({ownerId: user._id}).exec(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ import * as Tasks from '../models/task';
|
|||||||
import {
|
import {
|
||||||
NotFound,
|
NotFound,
|
||||||
BadRequest,
|
BadRequest,
|
||||||
|
NotAuthorized,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
|
import common from '../../common';
|
||||||
|
import {
|
||||||
|
model as Group,
|
||||||
|
} from '../models/group';
|
||||||
|
import apiError from '../libs/apiError';
|
||||||
|
|
||||||
|
const partyMembersFields = 'profile.name stats achievements items.special';
|
||||||
|
|
||||||
// @TODO: After refactoring individual spells, move quantity to the calculations
|
// @TODO: After refactoring individual spells, move quantity to the calculations
|
||||||
|
|
||||||
@@ -116,4 +124,91 @@ async function castUserSpell (res, req, party, partyMembers, targetId, user, spe
|
|||||||
return partyMembers;
|
return partyMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {castTaskSpell, castMultiTaskSpell, castSelfSpell, castPartySpell, castUserSpell};
|
async function castSpell (req, res, {isV3 = false}) {
|
||||||
|
const user = res.locals.user;
|
||||||
|
const spellId = req.params.spellId;
|
||||||
|
const 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);
|
||||||
|
let userToJson = results[0];
|
||||||
|
|
||||||
|
if (isV3) userToJson = await userToJson.toJSONWithInbox();
|
||||||
|
|
||||||
|
res.respond(200, {
|
||||||
|
user: userToJson,
|
||||||
|
task: results[1],
|
||||||
|
});
|
||||||
|
} else if (targetType === 'self') {
|
||||||
|
await castSelfSpell(req, user, spell, quantity);
|
||||||
|
|
||||||
|
let userToJson = user;
|
||||||
|
if (isV3) userToJson = await userToJson.toJSONWithInbox();
|
||||||
|
|
||||||
|
res.respond(200, {
|
||||||
|
user: userToJson,
|
||||||
|
});
|
||||||
|
} 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);
|
||||||
|
if (isV3) response.user = await response.user.toJSONWithInbox();
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
|
||||||
|
let userToJson = user;
|
||||||
|
if (isV3) userToJson = await userToJson.toJSONWithInbox();
|
||||||
|
|
||||||
|
res.respond(200, {
|
||||||
|
partyMembers: partyMembersRes,
|
||||||
|
user: userToJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
castTaskSpell,
|
||||||
|
castMultiTaskSpell,
|
||||||
|
castSelfSpell,
|
||||||
|
castPartySpell,
|
||||||
|
castUserSpell,
|
||||||
|
castSpell,
|
||||||
|
};
|
||||||
|
|||||||
242
website/server/libs/user/index.js
Normal file
242
website/server/libs/user/index.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import common from '../../../common';
|
||||||
|
import * as Tasks from '../../models/task';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {
|
||||||
|
BadRequest,
|
||||||
|
NotAuthorized,
|
||||||
|
} from '../../libs/errors';
|
||||||
|
import { model as User } from '../../models/user';
|
||||||
|
|
||||||
|
export async function get (req, res, { isV3 = false }) {
|
||||||
|
const user = res.locals.user;
|
||||||
|
let userToJSON;
|
||||||
|
|
||||||
|
if (isV3) {
|
||||||
|
userToJSON = await user.toJSONWithInbox();
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const 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;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const restrictedPUTSubPaths = [
|
||||||
|
'stats.class',
|
||||||
|
|
||||||
|
'preferences.disableClasses',
|
||||||
|
'preferences.sleep',
|
||||||
|
'preferences.webhooks',
|
||||||
|
];
|
||||||
|
|
||||||
|
_.each(restrictedPUTSubPaths, (removePath) => {
|
||||||
|
delete acceptablePUTPaths[removePath];
|
||||||
|
});
|
||||||
|
|
||||||
|
const 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',
|
||||||
|
};
|
||||||
|
|
||||||
|
function 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update (req, res, { isV3 = false }) {
|
||||||
|
const 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));
|
||||||
|
|
||||||
|
let userToJSON = user;
|
||||||
|
|
||||||
|
if (isV3) userToJSON = await user.toJSONWithInbox();
|
||||||
|
|
||||||
|
return res.respond(200, userToJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reset (req, res, { isV3 = false }) {
|
||||||
|
const user = res.locals.user;
|
||||||
|
|
||||||
|
const tasks = await Tasks.Task.find({
|
||||||
|
userId: user._id,
|
||||||
|
...Tasks.taskIsGroupOrChallengeQuery,
|
||||||
|
}).select('_id type challenge group').exec();
|
||||||
|
|
||||||
|
const resetRes = common.ops.reset(user, tasks);
|
||||||
|
if (isV3) {
|
||||||
|
resetRes[0].user = await resetRes[0].user.toJSONWithInbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reroll (req, res, { isV3 = false }) {
|
||||||
|
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();
|
||||||
|
const rerollRes = common.ops.reroll(user, tasks, req, res.analytics);
|
||||||
|
if (isV3) {
|
||||||
|
rerollRes[0].user = await rerollRes[0].user.toJSONWithInbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
let promises = tasks.map(task => task.save());
|
||||||
|
promises.push(user.save());
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
res.respond(200, ...rerollRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rebirth (req, res, { isV3 = false }) {
|
||||||
|
const user = res.locals.user;
|
||||||
|
const tasks = await Tasks.Task.find({
|
||||||
|
userId: user._id,
|
||||||
|
type: {$in: ['daily', 'habit', 'todo']},
|
||||||
|
...Tasks.taskIsGroupOrChallengeQuery,
|
||||||
|
}).exec();
|
||||||
|
|
||||||
|
const rebirthRes = common.ops.rebirth(user, tasks, req, res.analytics);
|
||||||
|
if (isV3) {
|
||||||
|
rebirthRes[0].user = await rebirthRes[0].user.toJSONWithInbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toSave = tasks.map(task => task.save());
|
||||||
|
|
||||||
|
toSave.push(user.save());
|
||||||
|
|
||||||
|
await Promise.all(toSave);
|
||||||
|
|
||||||
|
res.respond(200, ...rebirthRes);
|
||||||
|
}
|
||||||
@@ -33,7 +33,16 @@ app.use('/api/v3', v3Router);
|
|||||||
|
|
||||||
// A list of v3 routes in the format METHOD-URL to skip
|
// A list of v3 routes in the format METHOD-URL to skip
|
||||||
const v4RouterOverrides = [
|
const v4RouterOverrides = [
|
||||||
// 'GET-/status', Example to override the GET /status api call
|
'POST-/user/auth/local/register',
|
||||||
|
'GET-/user',
|
||||||
|
'PUT-/user',
|
||||||
|
'POST-/user/class/cast/:spellId',
|
||||||
|
'POST-/user/rebirth',
|
||||||
|
'POST-/user/reset',
|
||||||
|
'POST-/user/reroll',
|
||||||
|
'DELETE-/user/messages/:id',
|
||||||
|
'DELETE-/user/messages',
|
||||||
|
'POST-/coupons/enter/:code',
|
||||||
];
|
];
|
||||||
|
|
||||||
const v4Router = express.Router(); // eslint-disable-line new-cap
|
const v4Router = express.Router(); // eslint-disable-line new-cap
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import baseModel from '../libs/baseModel';
|
|
||||||
|
|
||||||
const schema = new mongoose.Schema({
|
|
||||||
timestamp: Date,
|
|
||||||
user: String,
|
|
||||||
text: String,
|
|
||||||
contributor: {type: mongoose.Schema.Types.Mixed},
|
|
||||||
backer: {type: mongoose.Schema.Types.Mixed},
|
|
||||||
uuid: String,
|
|
||||||
id: String,
|
|
||||||
groupId: {type: String, ref: 'Group'},
|
|
||||||
flags: {type: mongoose.Schema.Types.Mixed, default: {}},
|
|
||||||
flagCount: {type: Number, default: 0},
|
|
||||||
likes: {type: mongoose.Schema.Types.Mixed},
|
|
||||||
userStyles: {type: mongoose.Schema.Types.Mixed},
|
|
||||||
_meta: {type: mongoose.Schema.Types.Mixed},
|
|
||||||
}, {
|
|
||||||
minimize: false, // Allow for empty flags to be saved
|
|
||||||
});
|
|
||||||
|
|
||||||
schema.plugin(baseModel, {
|
|
||||||
noSet: ['_id'],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const model = mongoose.model('Chat', schema);
|
|
||||||
@@ -7,7 +7,11 @@ import {
|
|||||||
import shared from '../../common';
|
import shared from '../../common';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { model as Challenge} from './challenge';
|
import { model as Challenge} from './challenge';
|
||||||
import { model as Chat } from './chat';
|
import {
|
||||||
|
chatModel as Chat,
|
||||||
|
setUserStyles,
|
||||||
|
messageDefaults,
|
||||||
|
} from './message';
|
||||||
import * as Tasks from './task';
|
import * as Tasks from './task';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { removeFromArray } from '../libs/collectionManipulators';
|
import { removeFromArray } from '../libs/collectionManipulators';
|
||||||
@@ -72,17 +76,7 @@ export let schema = new Schema({
|
|||||||
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
||||||
type: {type: String, enum: ['guild', 'party'], required: true},
|
type: {type: String, enum: ['guild', 'party'], required: true},
|
||||||
privacy: {type: String, enum: ['private', 'public'], default: 'private', required: true},
|
privacy: {type: String, enum: ['private', 'public'], default: 'private', required: true},
|
||||||
chat: Array,
|
chat: Array, // Used for backward compatibility, but messages aren't stored here
|
||||||
/*
|
|
||||||
# [{
|
|
||||||
# timestamp: Date
|
|
||||||
# user: String
|
|
||||||
# text: String
|
|
||||||
# contributor: String
|
|
||||||
# uuid: String
|
|
||||||
# id: String
|
|
||||||
# }]
|
|
||||||
*/
|
|
||||||
leaderOnly: { // restrict group actions to leader (members can't do them)
|
leaderOnly: { // restrict group actions to leader (members can't do them)
|
||||||
challenges: {type: Boolean, default: false, required: true},
|
challenges: {type: Boolean, default: false, required: true},
|
||||||
// invites: {type: Boolean, default: false, required: true},
|
// invites: {type: Boolean, default: false, required: true},
|
||||||
@@ -473,81 +467,8 @@ schema.methods.getMemberCount = async function getMemberCount () {
|
|||||||
return await User.count(query).exec();
|
return await User.count(query).exec();
|
||||||
};
|
};
|
||||||
|
|
||||||
export function chatDefaults (msg, user) {
|
|
||||||
const id = shared.uuid();
|
|
||||||
const message = {
|
|
||||||
id,
|
|
||||||
_id: id,
|
|
||||||
text: msg.substring(0, 3000),
|
|
||||||
timestamp: Number(new Date()),
|
|
||||||
likes: {},
|
|
||||||
flags: {},
|
|
||||||
flagCount: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
_.defaults(message, {
|
|
||||||
uuid: user._id,
|
|
||||||
contributor: user.contributor && user.contributor.toObject(),
|
|
||||||
backer: user.backer && user.backer.toObject(),
|
|
||||||
user: user.profile.name,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.uuid = 'system';
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUserStyles (newMessage, user) {
|
|
||||||
let userStyles = {};
|
|
||||||
userStyles.items = {gear: {}};
|
|
||||||
|
|
||||||
let userCopy = user;
|
|
||||||
if (user.toObject) userCopy = user.toObject();
|
|
||||||
|
|
||||||
if (userCopy.items) {
|
|
||||||
userStyles.items.gear = {};
|
|
||||||
userStyles.items.gear.costume = Object.assign({}, userCopy.items.gear.costume);
|
|
||||||
userStyles.items.gear.equipped = Object.assign({}, userCopy.items.gear.equipped);
|
|
||||||
|
|
||||||
userStyles.items.currentMount = userCopy.items.currentMount;
|
|
||||||
userStyles.items.currentPet = userCopy.items.currentPet;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (userCopy.preferences) {
|
|
||||||
userStyles.preferences = {};
|
|
||||||
if (userCopy.preferences.style) userStyles.preferences.style = userCopy.preferences.style;
|
|
||||||
userStyles.preferences.hair = userCopy.preferences.hair;
|
|
||||||
userStyles.preferences.skin = userCopy.preferences.skin;
|
|
||||||
userStyles.preferences.shirt = userCopy.preferences.shirt;
|
|
||||||
userStyles.preferences.chair = userCopy.preferences.chair;
|
|
||||||
userStyles.preferences.size = userCopy.preferences.size;
|
|
||||||
userStyles.preferences.chair = userCopy.preferences.chair;
|
|
||||||
userStyles.preferences.background = userCopy.preferences.background;
|
|
||||||
userStyles.preferences.costume = userCopy.preferences.costume;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userCopy.stats) {
|
|
||||||
userStyles.stats = {};
|
|
||||||
userStyles.stats.class = userCopy.stats.class;
|
|
||||||
if (userCopy.stats.buffs) {
|
|
||||||
userStyles.stats.buffs = {
|
|
||||||
seafoam: userCopy.stats.buffs.seafoam,
|
|
||||||
shinySeed: userCopy.stats.buffs.shinySeed,
|
|
||||||
spookySparkles: userCopy.stats.buffs.spookySparkles,
|
|
||||||
snowball: userCopy.stats.buffs.snowball,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newMessage.userStyles = userStyles;
|
|
||||||
newMessage.markModified('userStyles');
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.methods.sendChat = function sendChat (message, user, metaData) {
|
schema.methods.sendChat = function sendChat (message, user, metaData) {
|
||||||
let newMessage = chatDefaults(message, user);
|
let newMessage = messageDefaults(message, user);
|
||||||
let newChatMessage = new Chat();
|
let newChatMessage = new Chat();
|
||||||
newChatMessage = Object.assign(newChatMessage, newMessage);
|
newChatMessage = Object.assign(newChatMessage, newMessage);
|
||||||
newChatMessage.groupId = this._id;
|
newChatMessage.groupId = this._id;
|
||||||
@@ -560,17 +481,6 @@ schema.methods.sendChat = function sendChat (message, user, metaData) {
|
|||||||
newChatMessage._meta = metaData;
|
newChatMessage._meta = metaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO: Completely remove the code below after migration
|
|
||||||
// this.chat.unshift(newMessage);
|
|
||||||
|
|
||||||
let maxCount = MAX_CHAT_COUNT;
|
|
||||||
|
|
||||||
if (this.isSubscribed()) {
|
|
||||||
maxCount = MAX_SUBBED_GROUP_CHAT_COUNT;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chat.splice(maxCount);
|
|
||||||
|
|
||||||
// do not send notifications for guilds with more than 5000 users and for the tavern
|
// do not send notifications for guilds with more than 5000 users and for the tavern
|
||||||
if (NO_CHAT_NOTIFICATIONS.indexOf(this._id) !== -1 || this.memberCount > LARGE_GROUP_COUNT_MESSAGE_CUTOFF) {
|
if (NO_CHAT_NOTIFICATIONS.indexOf(this._id) !== -1 || this.memberCount > LARGE_GROUP_COUNT_MESSAGE_CUTOFF) {
|
||||||
return newChatMessage;
|
return newChatMessage;
|
||||||
|
|||||||
124
website/server/models/message.js
Normal file
124
website/server/models/message.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import baseModel from '../libs/baseModel';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { defaults } from 'lodash';
|
||||||
|
|
||||||
|
const defaultSchema = () => ({
|
||||||
|
id: String,
|
||||||
|
timestamp: Date,
|
||||||
|
text: String,
|
||||||
|
|
||||||
|
// sender properties
|
||||||
|
user: String, // profile name
|
||||||
|
contributor: {type: mongoose.Schema.Types.Mixed},
|
||||||
|
backer: {type: mongoose.Schema.Types.Mixed},
|
||||||
|
uuid: String, // sender uuid
|
||||||
|
userStyles: {type: mongoose.Schema.Types.Mixed},
|
||||||
|
|
||||||
|
flags: {type: mongoose.Schema.Types.Mixed, default: {}},
|
||||||
|
flagCount: {type: Number, default: 0},
|
||||||
|
likes: {type: mongoose.Schema.Types.Mixed},
|
||||||
|
_meta: {type: mongoose.Schema.Types.Mixed},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatSchema = new mongoose.Schema({
|
||||||
|
...defaultSchema(),
|
||||||
|
groupId: {type: String, ref: 'Group'},
|
||||||
|
}, {
|
||||||
|
minimize: false, // Allow for empty flags to be saved
|
||||||
|
});
|
||||||
|
|
||||||
|
chatSchema.plugin(baseModel, {
|
||||||
|
noSet: ['_id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const inboxSchema = new mongoose.Schema({
|
||||||
|
sent: {type: Boolean, default: false}, // if the owner sent this message
|
||||||
|
// the uuid of the user where the message is stored,
|
||||||
|
// we store two copies of each inbox messages:
|
||||||
|
// one for the sender and one for the receiver
|
||||||
|
ownerId: {type: String, ref: 'User'},
|
||||||
|
...defaultSchema(),
|
||||||
|
}, {
|
||||||
|
minimize: false, // Allow for empty flags to be saved
|
||||||
|
});
|
||||||
|
|
||||||
|
inboxSchema.plugin(baseModel, {
|
||||||
|
noSet: ['_id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const chatModel = mongoose.model('Chat', chatSchema);
|
||||||
|
export const inboxModel = mongoose.model('Inbox', inboxSchema);
|
||||||
|
|
||||||
|
export function setUserStyles (newMessage, user) {
|
||||||
|
let userStyles = {};
|
||||||
|
userStyles.items = {gear: {}};
|
||||||
|
|
||||||
|
let userCopy = user;
|
||||||
|
if (user.toObject) userCopy = user.toObject();
|
||||||
|
|
||||||
|
if (userCopy.items) {
|
||||||
|
userStyles.items.gear = {};
|
||||||
|
userStyles.items.gear.costume = Object.assign({}, userCopy.items.gear.costume);
|
||||||
|
userStyles.items.gear.equipped = Object.assign({}, userCopy.items.gear.equipped);
|
||||||
|
|
||||||
|
userStyles.items.currentMount = userCopy.items.currentMount;
|
||||||
|
userStyles.items.currentPet = userCopy.items.currentPet;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (userCopy.preferences) {
|
||||||
|
userStyles.preferences = {};
|
||||||
|
if (userCopy.preferences.style) userStyles.preferences.style = userCopy.preferences.style;
|
||||||
|
userStyles.preferences.hair = userCopy.preferences.hair;
|
||||||
|
userStyles.preferences.skin = userCopy.preferences.skin;
|
||||||
|
userStyles.preferences.shirt = userCopy.preferences.shirt;
|
||||||
|
userStyles.preferences.chair = userCopy.preferences.chair;
|
||||||
|
userStyles.preferences.size = userCopy.preferences.size;
|
||||||
|
userStyles.preferences.chair = userCopy.preferences.chair;
|
||||||
|
userStyles.preferences.background = userCopy.preferences.background;
|
||||||
|
userStyles.preferences.costume = userCopy.preferences.costume;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userCopy.stats) {
|
||||||
|
userStyles.stats = {};
|
||||||
|
userStyles.stats.class = userCopy.stats.class;
|
||||||
|
if (userCopy.stats.buffs) {
|
||||||
|
userStyles.stats.buffs = {
|
||||||
|
seafoam: userCopy.stats.buffs.seafoam,
|
||||||
|
shinySeed: userCopy.stats.buffs.shinySeed,
|
||||||
|
spookySparkles: userCopy.stats.buffs.spookySparkles,
|
||||||
|
snowball: userCopy.stats.buffs.snowball,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newMessage.userStyles = userStyles;
|
||||||
|
newMessage.markModified('userStyles');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messageDefaults (msg, user) {
|
||||||
|
const id = uuid();
|
||||||
|
const message = {
|
||||||
|
id,
|
||||||
|
_id: id,
|
||||||
|
text: msg.substring(0, 3000),
|
||||||
|
timestamp: Number(new Date()),
|
||||||
|
likes: {},
|
||||||
|
flags: {},
|
||||||
|
flagCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
defaults(message, {
|
||||||
|
uuid: user._id,
|
||||||
|
contributor: user.contributor && user.contributor.toObject(),
|
||||||
|
backer: user.backer && user.backer.toObject(),
|
||||||
|
user: user.profile.name,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.uuid = 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
@@ -2,15 +2,21 @@ import moment from 'moment';
|
|||||||
import common from '../../../common';
|
import common from '../../../common';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
chatDefaults,
|
|
||||||
TAVERN_ID,
|
TAVERN_ID,
|
||||||
model as Group,
|
model as Group,
|
||||||
} from '../group';
|
} from '../group';
|
||||||
|
|
||||||
|
import {
|
||||||
|
messageDefaults,
|
||||||
|
setUserStyles,
|
||||||
|
inboxModel as Inbox,
|
||||||
|
} from '../message';
|
||||||
|
|
||||||
import { defaults, map, flatten, flow, compact, uniq, partialRight } from 'lodash';
|
import { defaults, map, flatten, flow, compact, uniq, partialRight } from 'lodash';
|
||||||
import { model as UserNotification } from '../userNotification';
|
import { model as UserNotification } from '../userNotification';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
import payments from '../../libs/payments/payments';
|
import payments from '../../libs/payments/payments';
|
||||||
|
import * as inboxLib from '../../libs/inbox';
|
||||||
import amazonPayments from '../../libs/payments/amazon';
|
import amazonPayments from '../../libs/payments/amazon';
|
||||||
import stripePayments from '../../libs/payments/stripe';
|
import stripePayments from '../../libs/payments/stripe';
|
||||||
import paypalPayments from '../../libs/payments/paypal';
|
import paypalPayments from '../../libs/payments/paypal';
|
||||||
@@ -101,16 +107,19 @@ schema.methods.getObjectionsToInteraction = function getObjectionsToInteraction
|
|||||||
* @return N/A
|
* @return N/A
|
||||||
*/
|
*/
|
||||||
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, options) {
|
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, options) {
|
||||||
let sender = this;
|
const sender = this;
|
||||||
let senderMsg = options.senderMsg || options.receiverMsg;
|
const senderMsg = options.senderMsg || options.receiverMsg;
|
||||||
// whether to save users after sending the message, defaults to true
|
// whether to save users after sending the message, defaults to true
|
||||||
let saveUsers = options.save === false ? false : true;
|
const saveUsers = options.save === false ? false : true;
|
||||||
|
|
||||||
|
const newReceiverMessage = new Inbox({
|
||||||
|
ownerId: userToReceiveMessage._id,
|
||||||
|
});
|
||||||
|
Object.assign(newReceiverMessage, messageDefaults(options.receiverMsg, sender));
|
||||||
|
setUserStyles(newReceiverMessage, sender);
|
||||||
|
|
||||||
const newMessageReceiver = chatDefaults(options.receiverMsg, sender);
|
|
||||||
common.refPush(userToReceiveMessage.inbox.messages, newMessageReceiver);
|
|
||||||
userToReceiveMessage.inbox.newMessages++;
|
userToReceiveMessage.inbox.newMessages++;
|
||||||
userToReceiveMessage._v++;
|
userToReceiveMessage._v++;
|
||||||
userToReceiveMessage.markModified('inbox.messages');
|
|
||||||
|
|
||||||
/* @TODO disabled until mobile is ready
|
/* @TODO disabled until mobile is ready
|
||||||
|
|
||||||
@@ -134,15 +143,31 @@ schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, o
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const newMessage = defaults({sent: true}, chatDefaults(senderMsg, userToReceiveMessage));
|
const sendingToYourself = userToReceiveMessage._id === sender._id;
|
||||||
common.refPush(sender.inbox.messages, newMessage);
|
|
||||||
sender.markModified('inbox.messages');
|
|
||||||
|
|
||||||
if (saveUsers) {
|
// Do not add the message twice when sending it to yourself
|
||||||
await Promise.all([userToReceiveMessage.save(), sender.save()]);
|
let newSenderMessage;
|
||||||
|
|
||||||
|
if (!sendingToYourself) {
|
||||||
|
newSenderMessage = new Inbox({
|
||||||
|
sent: true,
|
||||||
|
ownerId: sender._id,
|
||||||
|
});
|
||||||
|
Object.assign(newSenderMessage, messageDefaults(senderMsg, userToReceiveMessage));
|
||||||
|
setUserStyles(newSenderMessage, userToReceiveMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newMessage;
|
const promises = [newReceiverMessage.save()];
|
||||||
|
if (!sendingToYourself) promises.push(newSenderMessage.save());
|
||||||
|
|
||||||
|
if (saveUsers) {
|
||||||
|
promises.push(sender.save());
|
||||||
|
if (!sendingToYourself) promises.push(userToReceiveMessage.save());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return sendingToYourself ? newReceiverMessage : newSenderMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,3 +392,16 @@ schema.methods.isMemberOfGroupPlan = async function isMemberOfGroupPlan () {
|
|||||||
schema.methods.isAdmin = function isAdmin () {
|
schema.methods.isAdmin = function isAdmin () {
|
||||||
return this.contributor && this.contributor.admin;
|
return this.contributor && this.contributor.admin;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When converting to json add inbox messages from the Inbox collection
|
||||||
|
// for backward compatibility in API v3.
|
||||||
|
schema.methods.toJSONWithInbox = async function userToJSONWithInbox () {
|
||||||
|
const user = this;
|
||||||
|
const toJSON = user.toJSON();
|
||||||
|
|
||||||
|
if (toJSON.inbox) {
|
||||||
|
toJSON.inbox.messages = await inboxLib.getUserInbox(user, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toJSON;
|
||||||
|
};
|
||||||
|
|||||||
@@ -558,11 +558,13 @@ let schema = new Schema({
|
|||||||
tags: [TagSchema],
|
tags: [TagSchema],
|
||||||
|
|
||||||
inbox: {
|
inbox: {
|
||||||
newMessages: {type: Number, default: 0},
|
// messages are stored in the Inbox collection, this path will be removed
|
||||||
blocks: {type: Array, default: () => []},
|
// as soon as the migration has run and all the messages have been removed from here
|
||||||
messages: {type: Schema.Types.Mixed, default: () => {
|
messages: {type: Schema.Types.Mixed, default: () => {
|
||||||
return {};
|
return {};
|
||||||
}},
|
}},
|
||||||
|
newMessages: {type: Number, default: 0},
|
||||||
|
blocks: {type: Array, default: () => []},
|
||||||
optOut: {type: Boolean, default: false},
|
optOut: {type: Boolean, default: false},
|
||||||
},
|
},
|
||||||
tasksOrder: {
|
tasksOrder: {
|
||||||
|
|||||||
Reference in New Issue
Block a user