mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +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;
|
||||
});
|
||||
|
||||
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', () => {
|
||||
party.sendChat('message');
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('GET /inbox/messages', () => {
|
||||
|
||||
// message to yourself
|
||||
expect(messages[0].text).to.equal('fourth');
|
||||
expect(messages[0].sent).to.equal(false);
|
||||
expect(messages[0].uuid).to.equal(user._id);
|
||||
|
||||
expect(messages[1].text).to.equal('third');
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
generateUser,
|
||||
sleep,
|
||||
} 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', () => {
|
||||
const PET_QUEST = 'whale';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
generateUser,
|
||||
sleep,
|
||||
} 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', () => {
|
||||
const PET_QUEST = 'whale';
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
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';
|
||||
|
||||
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
sleep,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
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', () => {
|
||||
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 rebirth from './ops/rebirth';
|
||||
import blockUser from './ops/blockUser';
|
||||
import clearPMs from './ops/clearPMs';
|
||||
import deletePM from './ops/deletePM';
|
||||
import reroll from './ops/reroll';
|
||||
import reset from './ops/reset';
|
||||
import markPmsRead from './ops/markPMSRead';
|
||||
@@ -182,8 +180,6 @@ api.ops = {
|
||||
revive,
|
||||
rebirth,
|
||||
blockUser,
|
||||
clearPMs,
|
||||
deletePM,
|
||||
reroll,
|
||||
reset,
|
||||
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 updateTag from './updateTag';
|
||||
import deleteTag from './deleteTag';
|
||||
import clearPMs from './clearPMs';
|
||||
import deletePM from './deletePM';
|
||||
import blockUser from './blockUser';
|
||||
import feed from './feed';
|
||||
import releasePets from './releasePets';
|
||||
@@ -50,8 +48,6 @@ module.exports = {
|
||||
sortTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
clearPMs,
|
||||
deletePM,
|
||||
blockUser,
|
||||
feed,
|
||||
releasePets,
|
||||
|
||||
@@ -11,61 +11,22 @@ import {
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import logger from '../../libs/logger';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as Group } from '../../models/group';
|
||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||
import { sendTxn as sendTxnEmail } from '../../libs/email';
|
||||
import { decrypt, encrypt } from '../../libs/encryption';
|
||||
import { send as sendEmail } from '../../libs/email';
|
||||
import pusher from '../../libs/pusher';
|
||||
import common from '../../../common';
|
||||
import { validatePasswordResetCodeAndFindUser, convertToBcrypt} from '../../libs/password';
|
||||
import { encrypt } from '../../libs/encryption';
|
||||
import * as authLib from '../../libs/auth';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
|
||||
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
||||
const USERNAME_LENGTH_MIN = 1;
|
||||
const USERNAME_LENGTH_MAX = 20;
|
||||
|
||||
let api = {};
|
||||
|
||||
// When the user signed up after having been invited to a group, invite them automatically to the group
|
||||
async function _handleGroupInvitation (user, invite) {
|
||||
// wrapping the code in a try because we don't want it to prevent the user from signing up
|
||||
// that's why errors are not translated
|
||||
try {
|
||||
let {sentAt, id: groupId, inviter} = JSON.parse(decrypt(invite));
|
||||
|
||||
// check that the invite has not expired (after 7 days)
|
||||
if (sentAt && moment().subtract(7, 'days').isAfter(sentAt)) {
|
||||
let err = new Error('Invite expired.');
|
||||
err.privateData = invite;
|
||||
throw err;
|
||||
}
|
||||
|
||||
let group = await Group.getGroup({user, optionalMembership: true, groupId, fields: 'name type'});
|
||||
if (!group) throw new NotFound('Group not found.');
|
||||
|
||||
if (group.type === 'party') {
|
||||
user.invitations.party = {id: group._id, name: group.name, inviter};
|
||||
user.invitations.parties.push(user.invitations.party);
|
||||
} else {
|
||||
user.invitations.guilds.push({id: group._id, name: group.name, inviter});
|
||||
}
|
||||
|
||||
// award the inviter with 'Invited a Friend' achievement
|
||||
inviter = await User.findById(inviter);
|
||||
if (!inviter.achievements.invitedFriend) {
|
||||
inviter.achievements.invitedFriend = true;
|
||||
inviter.addNotification('INVITED_FRIEND_ACHIEVEMENT');
|
||||
await inviter.save();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function hasBackupAuth (user, networkToRemove) {
|
||||
if (user.auth.local.username) {
|
||||
return true;
|
||||
@@ -78,6 +39,8 @@ function hasBackupAuth (user, networkToRemove) {
|
||||
return hasAlternateNetwork;
|
||||
}
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/auth/local/register Register
|
||||
* @apiDescription Register a new user with email, login name, and password or attach local auth to a social user
|
||||
@@ -98,115 +61,7 @@ api.registerLocal = {
|
||||
})],
|
||||
url: '/user/auth/local/register',
|
||||
async handler (req, res) {
|
||||
let existingUser = res.locals.user; // If adding local auth to social user
|
||||
|
||||
req.checkBody({
|
||||
username: {
|
||||
notEmpty: true,
|
||||
errorMessage: res.t('missingUsername'),
|
||||
// TODO use the constants in the error message above
|
||||
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')},
|
||||
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')},
|
||||
},
|
||||
email: {
|
||||
notEmpty: true,
|
||||
errorMessage: res.t('missingEmail'),
|
||||
isEmail: {errorMessage: res.t('notAnEmail')},
|
||||
},
|
||||
password: {
|
||||
notEmpty: true,
|
||||
errorMessage: res.t('missingPassword'),
|
||||
equals: {options: [req.body.confirmPassword], errorMessage: res.t('passwordConfirmationMatch')},
|
||||
},
|
||||
});
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let { email, username, password } = req.body;
|
||||
|
||||
// Get the lowercase version of username to check that we do not have duplicates
|
||||
// So we can search for it in the database and then reject the choosen username if 1 or more results are found
|
||||
email = email.toLowerCase();
|
||||
username = username.trim();
|
||||
let lowerCaseUsername = username.toLowerCase();
|
||||
|
||||
// Search for duplicates using lowercase version of username
|
||||
let user = await User.findOne({$or: [
|
||||
{'auth.local.email': email},
|
||||
{'auth.local.lowerCaseUsername': lowerCaseUsername},
|
||||
]}, {'auth.local': 1}).exec();
|
||||
|
||||
if (user) {
|
||||
if (email === user.auth.local.email) throw new NotAuthorized(res.t('emailTaken'));
|
||||
// Check that the lowercase username isn't already used
|
||||
if (lowerCaseUsername === user.auth.local.lowerCaseUsername) throw new NotAuthorized(res.t('usernameTaken'));
|
||||
}
|
||||
|
||||
let hashed_password = await passwordUtils.bcryptHash(password); // eslint-disable-line camelcase
|
||||
let newUser = {
|
||||
auth: {
|
||||
local: {
|
||||
username,
|
||||
lowerCaseUsername,
|
||||
email,
|
||||
hashed_password, // eslint-disable-line camelcase,
|
||||
passwordHashMethod: 'bcrypt',
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
language: req.language,
|
||||
},
|
||||
};
|
||||
|
||||
if (existingUser) {
|
||||
let hasSocialAuth = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(network => {
|
||||
if (existingUser.auth.hasOwnProperty(network.key)) {
|
||||
return existingUser.auth[network.key].id;
|
||||
}
|
||||
});
|
||||
if (!hasSocialAuth) throw new NotAuthorized(res.t('onlySocialAttachLocal'));
|
||||
existingUser.auth.local = newUser.auth.local;
|
||||
newUser = existingUser;
|
||||
} else {
|
||||
newUser = new User(newUser);
|
||||
newUser.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
|
||||
}
|
||||
|
||||
// we check for partyInvite for backward compatibility
|
||||
if (req.query.groupInvite || req.query.partyInvite) {
|
||||
await _handleGroupInvitation(newUser, req.query.groupInvite || req.query.partyInvite);
|
||||
}
|
||||
|
||||
let savedUser = await newUser.save();
|
||||
|
||||
if (existingUser) {
|
||||
res.respond(200, savedUser.toJSON().auth.local); // We convert to toJSON to hide private fields
|
||||
} else {
|
||||
let userJSON = savedUser.toJSON();
|
||||
userJSON.newUser = true;
|
||||
res.respond(201, userJSON);
|
||||
}
|
||||
|
||||
// Clean previous email preferences and send welcome email
|
||||
EmailUnsubscription
|
||||
.remove({email: savedUser.auth.local.email})
|
||||
.then(() => {
|
||||
if (!existingUser) sendTxnEmail(savedUser, 'welcome');
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
res.analytics.track('register', {
|
||||
category: 'acquisition',
|
||||
type: 'local',
|
||||
gaLabel: 'local',
|
||||
uuid: savedUser._id,
|
||||
headers: req.headers,
|
||||
user: savedUser,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
await authLib.registerLocal(req, res, { isV3: true });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -398,9 +253,7 @@ api.loginSocial = {
|
||||
*/
|
||||
api.pusherAuth = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/auth/pusher',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -468,9 +321,7 @@ api.pusherAuth = {
|
||||
**/
|
||||
api.updateUsername = {
|
||||
method: 'PUT',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/auth/update-username',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -524,9 +375,7 @@ api.updateUsername = {
|
||||
**/
|
||||
api.updatePassword = {
|
||||
method: 'PUT',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/auth/update-password',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -636,9 +485,7 @@ api.resetPassword = {
|
||||
*/
|
||||
api.updateEmail = {
|
||||
method: 'PUT',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/auth/update-email',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -725,9 +572,7 @@ api.resetPasswordSetNewOne = {
|
||||
api.deleteSocial = {
|
||||
method: 'DELETE',
|
||||
url: '/user/auth/social/:network',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let network = req.params.network;
|
||||
|
||||
@@ -184,9 +184,7 @@ let api = {};
|
||||
api.createChallenge = {
|
||||
method: 'POST',
|
||||
url: '/challenges',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -235,9 +233,7 @@ api.createChallenge = {
|
||||
api.joinChallenge = {
|
||||
method: 'POST',
|
||||
url: '/challenges/:challengeId/join',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -294,9 +290,7 @@ api.joinChallenge = {
|
||||
api.leaveChallenge = {
|
||||
method: 'POST',
|
||||
url: '/challenges/:challengeId/leave',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let keep = req.body.keep === 'remove-all' ? 'remove-all' : 'keep-all';
|
||||
@@ -345,9 +339,7 @@ api.leaveChallenge = {
|
||||
api.getUserChallenges = {
|
||||
method: 'GET',
|
||||
url: '/challenges/user',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const CHALLENGES_PER_PAGE = 10;
|
||||
const page = req.query.page;
|
||||
@@ -508,9 +500,7 @@ api.getGroupChallenges = {
|
||||
api.getChallenge = {
|
||||
method: 'GET',
|
||||
url: '/challenges/:challengeId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
|
||||
@@ -664,9 +654,7 @@ api.exportChallengeCsv = {
|
||||
api.updateChallenge = {
|
||||
method: 'PUT',
|
||||
url: '/challenges/:challengeId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
|
||||
@@ -708,9 +696,7 @@ api.updateChallenge = {
|
||||
api.deleteChallenge = {
|
||||
method: 'DELETE',
|
||||
url: '/challenges/:challengeId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -755,9 +741,7 @@ api.deleteChallenge = {
|
||||
api.selectChallengeWinner = {
|
||||
method: 'POST',
|
||||
url: '/challenges/:challengeId/selectWinner/:winnerId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -806,9 +790,7 @@ api.selectChallengeWinner = {
|
||||
api.cloneChallenge = {
|
||||
method: 'POST',
|
||||
url: '/challenges/:challengeId/clone',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { model as Group } from '../../models/group';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as Chat } from '../../models/chat';
|
||||
import { chatModel as Chat } from '../../models/message';
|
||||
import {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
@@ -62,9 +62,7 @@ function textContainsBannedSlur (message) {
|
||||
api.getChat = {
|
||||
method: 'GET',
|
||||
url: '/groups/:groupId/chat',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -103,9 +101,7 @@ function getBannedWordsFromText (message) {
|
||||
api.postChat = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/chat',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let groupId = req.params.groupId;
|
||||
@@ -227,9 +223,7 @@ api.postChat = {
|
||||
api.likeChat = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/chat/:chatId/like',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let groupId = req.params.groupId;
|
||||
@@ -286,9 +280,7 @@ api.likeChat = {
|
||||
api.flagChat = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/chat/:chatId/flag',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const chatReporter = chatReporterFactory('Group', req, res);
|
||||
const message = await chatReporter.flag();
|
||||
@@ -317,9 +309,7 @@ api.flagChat = {
|
||||
api.clearChatFlags = {
|
||||
method: 'Post',
|
||||
url: '/groups/:groupId/chat/:chatId/clearflags',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let groupId = req.params.groupId;
|
||||
@@ -389,9 +379,7 @@ api.clearChatFlags = {
|
||||
api.seenChat = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/chat/seen',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let groupId = req.params.groupId;
|
||||
@@ -457,9 +445,7 @@ api.seenChat = {
|
||||
api.deleteChat = {
|
||||
method: 'DELETE',
|
||||
url: '/groups/:groupId/chat/:chatId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let groupId = req.params.groupId;
|
||||
|
||||
@@ -4,10 +4,11 @@ import {
|
||||
authWithSession,
|
||||
} from '../../middlewares/auth';
|
||||
import { ensureSudo } from '../../middlewares/ensureAccessRight';
|
||||
import { model as Coupon } from '../../models/coupon';
|
||||
import _ from 'lodash';
|
||||
import * as couponsLib from '../../libs/coupons';
|
||||
import couponCode from 'coupon-code';
|
||||
import apiError from '../../libs/apiError';
|
||||
import { model as Coupon } from '../../models/coupon';
|
||||
|
||||
let api = {};
|
||||
|
||||
@@ -68,9 +69,7 @@ api.getCoupons = {
|
||||
api.generateCoupons = {
|
||||
method: 'POST',
|
||||
url: '/coupons/generate/:event',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
}), ensureSudo],
|
||||
middlewares: [authWithHeaders(), ensureSudo],
|
||||
async handler (req, res) {
|
||||
req.checkParams('event', apiError('eventRequired')).notEmpty();
|
||||
req.checkQuery('count', apiError('countRequired')).notEmpty().isNumeric();
|
||||
@@ -83,6 +82,8 @@ api.generateCoupons = {
|
||||
},
|
||||
};
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/coupons/enter/:code Redeem a coupon code
|
||||
* @apiName RedeemCouponCode
|
||||
@@ -95,19 +96,12 @@ api.generateCoupons = {
|
||||
api.enterCouponCode = {
|
||||
method: 'POST',
|
||||
url: '/coupons/enter/:code',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
await Coupon.apply(user, req, req.params.code);
|
||||
res.respond(200, user);
|
||||
const user = res.locals.user;
|
||||
await couponsLib.enterCode(req, res, user);
|
||||
const userToJSON = await user.toJSONWithInbox();
|
||||
res.respond(200, userToJSON);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -125,7 +119,6 @@ api.validateCoupon = {
|
||||
url: '/coupons/validate/:code',
|
||||
middlewares: [authWithHeaders({
|
||||
optional: true,
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
async handler (req, res) {
|
||||
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
|
||||
|
||||
@@ -109,9 +109,7 @@ let api = {};
|
||||
api.createGroup = {
|
||||
method: 'POST',
|
||||
url: '/groups',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let group = new Group(Group.sanitize(req.body));
|
||||
@@ -182,9 +180,7 @@ api.createGroup = {
|
||||
api.createGroupPlan = {
|
||||
method: 'POST',
|
||||
url: '/groups/create-plan',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let group = new Group(Group.sanitize(req.body.groupToCreate));
|
||||
@@ -293,9 +289,7 @@ api.createGroupPlan = {
|
||||
api.getGroups = {
|
||||
method: 'GET',
|
||||
url: '/groups',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -443,9 +437,7 @@ api.getGroup = {
|
||||
api.updateGroup = {
|
||||
method: 'PUT',
|
||||
url: '/groups/:groupId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -508,9 +500,7 @@ api.updateGroup = {
|
||||
api.joinGroup = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/join',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let inviter;
|
||||
@@ -682,9 +672,7 @@ api.joinGroup = {
|
||||
api.rejectGroupInvite = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/reject-invite',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -759,9 +747,7 @@ function _removeMessagesFromMember (member, groupId) {
|
||||
api.leaveGroup = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/leave',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
@@ -848,9 +834,7 @@ function _sendMessageToRemoved (group, removedUser, message, isInGroup) {
|
||||
api.removeGroupMember = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/removeMember/:memberId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -1176,7 +1160,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
|
||||
api.inviteToGroup = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/invite',
|
||||
middlewares: [authWithHeaders({})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -1249,9 +1233,7 @@ api.inviteToGroup = {
|
||||
api.addGroupManager = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/add-manager',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let managerId = req.body.managerId;
|
||||
@@ -1300,9 +1282,7 @@ api.addGroupManager = {
|
||||
api.removeGroupManager = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/remove-manager',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let managerId = req.body.managerId;
|
||||
@@ -1355,9 +1335,7 @@ api.removeGroupManager = {
|
||||
api.getGroupPlans = {
|
||||
method: 'GET',
|
||||
url: '/group-plans',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
|
||||
@@ -61,9 +61,7 @@ let api = {};
|
||||
api.getPatrons = {
|
||||
method: 'GET',
|
||||
url: '/hall/patrons',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('page').optional().isInt({min: 0}, apiError('queryPageInteger'));
|
||||
|
||||
@@ -123,9 +121,7 @@ api.getPatrons = {
|
||||
api.getHeroes = {
|
||||
method: 'GET',
|
||||
url: '/hall/heroes',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let heroes = await User
|
||||
.find({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { toArray, orderBy } from 'lodash';
|
||||
import * as inboxLib from '../../libs/inbox';
|
||||
|
||||
let api = {};
|
||||
|
||||
@@ -7,7 +7,6 @@ let api = {};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/inbox/messages Get inbox messages for a user
|
||||
* @apiPrivate
|
||||
* @apiName GetInboxMessages
|
||||
* @apiGroup Inbox
|
||||
* @apiDescription Get inbox messages for a user
|
||||
@@ -19,10 +18,11 @@ api.getInboxMessages = {
|
||||
url: '/inbox/messages',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const messagesObj = res.locals.user.inbox.messages;
|
||||
const messagesArray = orderBy(toArray(messagesObj), ['timestamp'], ['desc']);
|
||||
const user = res.locals.user;
|
||||
|
||||
res.respond(200, messagesArray);
|
||||
const userInbox = await inboxLib.getUserInbox(user);
|
||||
|
||||
res.respond(200, userInbox);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -379,9 +379,7 @@ function _getMembersForItem (type) {
|
||||
api.getMembersForGroup = {
|
||||
method: 'GET',
|
||||
url: '/groups/:groupId/members',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
handler: _getMembersForItem('group-members'),
|
||||
};
|
||||
|
||||
@@ -417,9 +415,7 @@ api.getMembersForGroup = {
|
||||
api.getInvitesForGroup = {
|
||||
method: 'GET',
|
||||
url: '/groups/:groupId/invites',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
handler: _getMembersForItem('group-invites'),
|
||||
};
|
||||
|
||||
@@ -445,9 +441,7 @@ api.getInvitesForGroup = {
|
||||
api.getMembersForChallenge = {
|
||||
method: 'GET',
|
||||
url: '/challenges/:challengeId/members',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
handler: _getMembersForItem('challenge-members'),
|
||||
};
|
||||
|
||||
@@ -509,9 +503,7 @@ api.getMembersForChallenge = {
|
||||
api.getChallengeMemberProgress = {
|
||||
method: 'GET',
|
||||
url: '/challenges/:challengeId/members/:memberId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
@@ -594,7 +586,7 @@ api.getObjectionsToInteraction = {
|
||||
* @apiParam (Body) {String} message Body parameter - The message
|
||||
* @apiParam (Body) {UUID} toUserId Body parameter - The user to contact
|
||||
*
|
||||
* @apiSuccess {Object} data An empty Object
|
||||
* @apiSuccess {Object} data.message The message just sent
|
||||
*
|
||||
* @apiUse UserNotFound
|
||||
*/
|
||||
@@ -617,7 +609,7 @@ api.sendPrivateMessage = {
|
||||
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
|
||||
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
|
||||
|
||||
const newMessage = await sender.sendMessage(receiver, { receiverMsg: message });
|
||||
const messageSent = await sender.sendMessage(receiver, { receiverMsg: message });
|
||||
|
||||
if (receiver.preferences.emailNotifications.newPM !== false) {
|
||||
sendTxnEmail(receiver, 'new-pm', [
|
||||
@@ -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;
|
||||
sender.balance -= amount;
|
||||
// @TODO necessary? Also saved when sending the inbox message
|
||||
let promises = [receiver.save(), sender.save()];
|
||||
await Promise.all(promises);
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ api.getNews = {
|
||||
*/
|
||||
api.tellMeLaterNews = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/news/tell-me-later',
|
||||
async handler (req, res) {
|
||||
const user = res.locals.user;
|
||||
|
||||
@@ -23,9 +23,7 @@ let api = {};
|
||||
api.readNotification = {
|
||||
method: 'POST',
|
||||
url: '/notifications/:notificationId/read',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -67,9 +65,7 @@ api.readNotification = {
|
||||
api.readNotifications = {
|
||||
method: 'POST',
|
||||
url: '/notifications/read',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -117,9 +113,7 @@ api.readNotifications = {
|
||||
api.seeNotification = {
|
||||
method: 'POST',
|
||||
url: '/notifications/:notificationId/see',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -168,9 +162,7 @@ api.seeNotification = {
|
||||
api.seeNotifications = {
|
||||
method: 'POST',
|
||||
url: '/notifications/see',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
|
||||
@@ -22,9 +22,7 @@ let api = {};
|
||||
api.addPushDevice = {
|
||||
method: 'POST',
|
||||
url: '/user/push-devices',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const user = res.locals.user;
|
||||
|
||||
@@ -72,9 +70,7 @@ api.addPushDevice = {
|
||||
api.removePushDevice = {
|
||||
method: 'DELETE',
|
||||
url: '/user/push-devices/:regId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const user = res.locals.user;
|
||||
|
||||
|
||||
@@ -55,9 +55,7 @@ let api = {};
|
||||
api.inviteToQuest = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/quests/invite/:questKey',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let questKey = req.params.questKey;
|
||||
@@ -171,9 +169,7 @@ api.inviteToQuest = {
|
||||
api.acceptQuest = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/quests/accept',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -232,9 +228,7 @@ api.acceptQuest = {
|
||||
api.rejectQuest = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/quests/reject',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -297,9 +291,7 @@ api.rejectQuest = {
|
||||
api.forceStart = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/quests/force-start',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -357,9 +349,7 @@ api.forceStart = {
|
||||
api.cancelQuest = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/quests/cancel',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
// Cancel a quest BEFORE it has begun (i.e., in the invitation stage)
|
||||
// Quest scroll has not yet left quest owner's inventory so no need to return it.
|
||||
@@ -413,9 +403,7 @@ api.cancelQuest = {
|
||||
api.abortQuest = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/quests/abort',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
// Abort a quest AFTER it has begun (see questCancel for BEFORE)
|
||||
let user = res.locals.user;
|
||||
@@ -475,9 +463,7 @@ api.abortQuest = {
|
||||
api.leaveQuest = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/quests/leave',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let groupId = req.params.groupId;
|
||||
|
||||
@@ -15,9 +15,7 @@ let api = {};
|
||||
api.getMarketItems = {
|
||||
method: 'GET',
|
||||
url: '/shops/market',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -38,9 +36,7 @@ api.getMarketItems = {
|
||||
api.getMarketGear = {
|
||||
method: 'GET',
|
||||
url: '/shops/market-gear',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -64,9 +60,7 @@ api.getMarketGear = {
|
||||
api.getQuestShopItems = {
|
||||
method: 'GET',
|
||||
url: '/shops/quests',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -88,9 +82,7 @@ api.getQuestShopItems = {
|
||||
api.getTimeTravelerShopItems = {
|
||||
method: 'GET',
|
||||
url: '/shops/time-travelers',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -112,9 +104,7 @@ api.getTimeTravelerShopItems = {
|
||||
api.getSeasonalShopItems = {
|
||||
method: 'GET',
|
||||
url: '/shops/seasonal',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -136,9 +126,7 @@ api.getSeasonalShopItems = {
|
||||
api.getBackgroundShopItems = {
|
||||
method: 'GET',
|
||||
url: '/shops/backgrounds',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
|
||||
@@ -38,9 +38,7 @@ let api = {};
|
||||
api.createTag = {
|
||||
method: 'POST',
|
||||
url: '/tags',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -66,9 +64,7 @@ api.createTag = {
|
||||
api.getTags = {
|
||||
method: 'GET',
|
||||
url: '/tags',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
res.respond(200, user.tags);
|
||||
@@ -93,9 +89,7 @@ api.getTags = {
|
||||
api.getTag = {
|
||||
method: 'GET',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -132,9 +126,7 @@ api.getTag = {
|
||||
api.updateTag = {
|
||||
method: 'PUT',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -176,9 +168,7 @@ api.updateTag = {
|
||||
api.reorderTags = {
|
||||
method: 'POST',
|
||||
url: '/reorder-tags',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -217,9 +207,7 @@ api.reorderTags = {
|
||||
api.deleteTag = {
|
||||
method: 'DELETE',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
|
||||
@@ -158,9 +158,7 @@ let requiredGroupFields = '_id leader tasksOrder name';
|
||||
api.createUserTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks/user',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let tasks = await createTasks(req, res, {user});
|
||||
@@ -232,9 +230,7 @@ api.createUserTasks = {
|
||||
api.createChallengeTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks/challenge/:challengeId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
|
||||
@@ -328,9 +324,7 @@ api.getUserTasks = {
|
||||
api.getChallengeTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/challenge/:challengeId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
@@ -380,9 +374,7 @@ api.getChallengeTasks = {
|
||||
api.getTask = {
|
||||
method: 'GET',
|
||||
url: '/tasks/:taskId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let taskId = req.params.taskId;
|
||||
@@ -436,9 +428,7 @@ api.getTask = {
|
||||
api.updateTask = {
|
||||
method: 'PUT',
|
||||
url: '/tasks/:taskId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
@@ -552,9 +542,7 @@ api.updateTask = {
|
||||
api.scoreTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/score/:direction',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('direction', res.t('directionUpDown')).notEmpty().isIn(['up', 'down']);
|
||||
|
||||
@@ -728,9 +716,7 @@ api.scoreTask = {
|
||||
api.moveTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/move/to/:position',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||
@@ -799,9 +785,7 @@ api.moveTask = {
|
||||
api.addChecklistItem = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/checklist',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
@@ -861,9 +845,7 @@ api.addChecklistItem = {
|
||||
api.scoreCheckListItem = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/checklist/:itemId/score',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -917,9 +899,7 @@ api.scoreCheckListItem = {
|
||||
api.updateChecklistItem = {
|
||||
method: 'PUT',
|
||||
url: '/tasks/:taskId/checklist/:itemId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
@@ -984,9 +964,7 @@ api.updateChecklistItem = {
|
||||
api.removeChecklistItem = {
|
||||
method: 'DELETE',
|
||||
url: '/tasks/:taskId/checklist/:itemId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
@@ -1049,9 +1027,7 @@ api.removeChecklistItem = {
|
||||
api.addTagToTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/tags/:tagId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -1100,9 +1076,7 @@ api.addTagToTask = {
|
||||
api.removeTagFromTask = {
|
||||
method: 'DELETE',
|
||||
url: '/tasks/:taskId/tags/:tagId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -1147,9 +1121,7 @@ api.removeTagFromTask = {
|
||||
api.unlinkAllTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks/unlink-all/:challengeId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('keep', apiError('keepOrRemoveAll')).notEmpty().isIn(['keep-all', 'remove-all']);
|
||||
@@ -1216,9 +1188,7 @@ api.unlinkAllTasks = {
|
||||
api.unlinkOneTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/unlink-one/:taskId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('keep', apiError('keepOrRemove')).notEmpty().isIn(['keep', 'remove']);
|
||||
@@ -1268,9 +1238,7 @@ api.unlinkOneTask = {
|
||||
api.clearCompletedTodos = {
|
||||
method: 'POST',
|
||||
url: '/tasks/clearCompletedTodos',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -1321,9 +1289,7 @@ api.clearCompletedTodos = {
|
||||
api.deleteTask = {
|
||||
method: 'DELETE',
|
||||
url: '/tasks/:taskId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
|
||||
@@ -42,9 +42,7 @@ let api = {};
|
||||
api.createGroupTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks/group/:groupId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||
|
||||
@@ -88,9 +86,7 @@ api.createGroupTasks = {
|
||||
api.getGroupTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/group/:groupId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);
|
||||
@@ -123,9 +119,7 @@ api.getGroupTasks = {
|
||||
api.groupMoveTask = {
|
||||
method: 'POST',
|
||||
url: '/group-tasks/:taskId/move/to/:position',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||
@@ -176,9 +170,7 @@ api.groupMoveTask = {
|
||||
api.assignTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/assign/:assignedUserId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
@@ -238,9 +230,7 @@ api.assignTask = {
|
||||
api.unassignTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/unassign/:assignedUserId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
@@ -290,9 +280,7 @@ api.unassignTask = {
|
||||
api.approveTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/approve/:userId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
@@ -390,9 +378,7 @@ api.approveTask = {
|
||||
api.taskNeedsWork = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/needs-work/:userId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
@@ -489,9 +475,7 @@ api.taskNeedsWork = {
|
||||
api.getGroupApprovals = {
|
||||
method: 'GET',
|
||||
url: '/approvals/group/:groupId',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@ import {
|
||||
basicFields as basicGroupFields,
|
||||
model as Group,
|
||||
} from '../../models/group';
|
||||
import {
|
||||
model as User,
|
||||
} from '../../models/user';
|
||||
import * as Tasks from '../../models/task';
|
||||
import _ from 'lodash';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
@@ -22,6 +19,8 @@ import {
|
||||
sendTxn as txnEmail,
|
||||
} from '../../libs/email';
|
||||
import Queue from '../../libs/queue';
|
||||
import * as inboxLib from '../../libs/inbox';
|
||||
import * as userLib from '../../libs/user';
|
||||
import nconf from 'nconf';
|
||||
import get from 'lodash/get';
|
||||
|
||||
@@ -35,6 +34,8 @@ const DELETE_CONFIRMATION = 'DELETE';
|
||||
|
||||
let api = {};
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/user Get the authenticated user's profile
|
||||
* @apiName UserGet
|
||||
@@ -47,7 +48,7 @@ let api = {};
|
||||
* Flags (including armoire, tutorial, tour etc...)
|
||||
* Guilds
|
||||
* History (including timestamps and values)
|
||||
* Inbox (includes message history)
|
||||
* Inbox
|
||||
* Invitations (to parties/guilds)
|
||||
* Items (character's full inventory)
|
||||
* New Messages (flags for groups/guilds that have new messages)
|
||||
@@ -83,20 +84,7 @@ api.getUser = {
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let userToJSON = user.toJSON();
|
||||
|
||||
// Remove apiToken from response TODO make it private at the user level? returned in signup/login
|
||||
delete userToJSON.apiToken;
|
||||
|
||||
if (!req.query.userFields) {
|
||||
let {daysMissed} = user.daysUserHasMissed(new Date(), req);
|
||||
userToJSON.needsCron = false;
|
||||
if (daysMissed > 0) userToJSON.needsCron = true;
|
||||
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
|
||||
}
|
||||
|
||||
return res.respond(200, userToJSON);
|
||||
await userLib.get(req, res, { isV3: true });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -128,9 +116,7 @@ api.getUser = {
|
||||
*/
|
||||
api.getBuyList = {
|
||||
method: 'GET',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/inventory/buy',
|
||||
async handler (req, res) {
|
||||
let list = _.cloneDeep(common.updateStore(res.locals.user));
|
||||
@@ -173,9 +159,7 @@ api.getBuyList = {
|
||||
*/
|
||||
api.getInAppRewardsList = {
|
||||
method: 'GET',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/in-app-rewards',
|
||||
async handler (req, res) {
|
||||
let list = common.inAppRewards(res.locals.user);
|
||||
@@ -191,78 +175,7 @@ api.getInAppRewardsList = {
|
||||
},
|
||||
};
|
||||
|
||||
let updatablePaths = [
|
||||
'_ABtests.counter',
|
||||
|
||||
'flags.customizationsNotification',
|
||||
'flags.showTour',
|
||||
'flags.tour',
|
||||
'flags.tutorial',
|
||||
'flags.communityGuidelinesAccepted',
|
||||
'flags.welcomed',
|
||||
'flags.cardReceived',
|
||||
'flags.warnedLowHealth',
|
||||
'flags.newStuff',
|
||||
|
||||
'achievements',
|
||||
|
||||
'party.order',
|
||||
'party.orderAscending',
|
||||
'party.quest.completed',
|
||||
'party.quest.RSVPNeeded',
|
||||
|
||||
'preferences',
|
||||
'profile',
|
||||
'stats',
|
||||
'inbox.optOut',
|
||||
'tags',
|
||||
];
|
||||
|
||||
// This tells us for which paths users can call `PUT /user`.
|
||||
// The trick here is to only accept leaf paths, not root/intermediate paths (see http://goo.gl/OEzkAs)
|
||||
let acceptablePUTPaths = _.reduce(require('./../../models/user').schema.paths, (accumulator, val, leaf) => {
|
||||
let found = _.find(updatablePaths, (rootPath) => {
|
||||
return leaf.indexOf(rootPath) === 0;
|
||||
});
|
||||
|
||||
if (found) accumulator[leaf] = true;
|
||||
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
let restrictedPUTSubPaths = [
|
||||
'stats.class',
|
||||
|
||||
'preferences.disableClasses',
|
||||
'preferences.sleep',
|
||||
'preferences.webhooks',
|
||||
];
|
||||
|
||||
_.each(restrictedPUTSubPaths, (removePath) => {
|
||||
delete acceptablePUTPaths[removePath];
|
||||
});
|
||||
|
||||
let requiresPurchase = {
|
||||
'preferences.background': 'background',
|
||||
'preferences.shirt': 'shirt',
|
||||
'preferences.size': 'size',
|
||||
'preferences.skin': 'skin',
|
||||
'preferences.hair.bangs': 'hair.bangs',
|
||||
'preferences.hair.base': 'hair.base',
|
||||
'preferences.hair.beard': 'hair.beard',
|
||||
'preferences.hair.color': 'hair.color',
|
||||
'preferences.hair.flower': 'hair.flower',
|
||||
'preferences.hair.mustache': 'hair.mustache',
|
||||
};
|
||||
|
||||
let checkPreferencePurchase = (user, path, item) => {
|
||||
let itemPath = `${path}.${item}`;
|
||||
let appearance = _.get(common.content.appearances, itemPath);
|
||||
if (!appearance) return false;
|
||||
if (appearance.price === 0) return true;
|
||||
|
||||
return _.get(user.purchased, itemPath);
|
||||
};
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {put} /api/v3/user Update the user
|
||||
@@ -297,67 +210,7 @@ api.updateUser = {
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
let promisesForTagsRemoval = [];
|
||||
|
||||
_.each(req.body, (val, key) => {
|
||||
let purchasable = requiresPurchase[key];
|
||||
|
||||
if (purchasable && !checkPreferencePurchase(user, purchasable, val)) {
|
||||
throw new NotAuthorized(res.t('mustPurchaseToSet', { val, key }));
|
||||
}
|
||||
|
||||
if (acceptablePUTPaths[key] && key !== 'tags') {
|
||||
_.set(user, key, val);
|
||||
} else if (key === 'tags') {
|
||||
if (!Array.isArray(val)) throw new BadRequest('mustBeArray');
|
||||
|
||||
const removedTagsIds = [];
|
||||
|
||||
const oldTags = [];
|
||||
|
||||
// Keep challenge and group tags
|
||||
user.tags.forEach(t => {
|
||||
if (t.group) {
|
||||
oldTags.push(t);
|
||||
} else {
|
||||
removedTagsIds.push(t.id);
|
||||
}
|
||||
});
|
||||
|
||||
user.tags = oldTags;
|
||||
|
||||
val.forEach(t => {
|
||||
let oldI = removedTagsIds.findIndex(id => id === t.id);
|
||||
if (oldI > -1) {
|
||||
removedTagsIds.splice(oldI, 1);
|
||||
}
|
||||
|
||||
user.tags.push(t);
|
||||
});
|
||||
|
||||
// Remove from all the tasks
|
||||
// NOTE each tag to remove requires a query
|
||||
|
||||
promisesForTagsRemoval = removedTagsIds.map(tagId => {
|
||||
return Tasks.Task.update({
|
||||
userId: user._id,
|
||||
}, {
|
||||
$pull: {
|
||||
tags: tagId,
|
||||
},
|
||||
}, {multi: true}).exec();
|
||||
});
|
||||
} else {
|
||||
throw new NotAuthorized(res.t('messageUserOperationProtected', { operation: key }));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await Promise.all([user.save()].concat(promisesForTagsRemoval));
|
||||
|
||||
return res.respond(200, user);
|
||||
await userLib.update(req, res, { isV3: true });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -488,7 +341,7 @@ api.getUserAnonymized = {
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/anonymized',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user.toJSON();
|
||||
let user = await res.locals.user.toJSONWithInbox();
|
||||
user.stats.toNextLevel = common.tnl(user.stats.lvl);
|
||||
user.stats.maxHealth = common.maxHealth;
|
||||
user.stats.maxMP = common.statsComputed(res.locals.user).maxMP;
|
||||
@@ -513,6 +366,7 @@ api.getUserAnonymized = {
|
||||
_.forEach(user.inbox.messages, (msg) => {
|
||||
msg.text = 'inbox message text';
|
||||
});
|
||||
|
||||
_.forEach(user.tags, (tag) => {
|
||||
tag.name = 'tag';
|
||||
tag.challenge = 'challenge';
|
||||
@@ -556,9 +410,7 @@ api.getUserAnonymized = {
|
||||
*/
|
||||
api.sleep = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/sleep',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -602,9 +454,7 @@ const buyKnownKeys = ['armoire', 'mystery', 'potion', 'quest', 'special'];
|
||||
*/
|
||||
api.buy = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/buy/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -668,9 +518,7 @@ api.buy = {
|
||||
*/
|
||||
api.buyGear = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/buy-gear/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -710,9 +558,7 @@ api.buyGear = {
|
||||
*/
|
||||
api.buyArmoire = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/buy-armoire',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -752,9 +598,7 @@ api.buyArmoire = {
|
||||
*/
|
||||
api.buyHealthPotion = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/buy-health-potion',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -796,9 +640,7 @@ api.buyHealthPotion = {
|
||||
*/
|
||||
api.buyMysterySet = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/buy-mystery-set/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -841,9 +683,7 @@ api.buyMysterySet = {
|
||||
*/
|
||||
api.buyQuest = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/buy-quest/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -883,9 +723,7 @@ api.buyQuest = {
|
||||
*/
|
||||
api.buySpecialSpell = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/buy-special-spell/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -929,9 +767,7 @@ api.buySpecialSpell = {
|
||||
*/
|
||||
api.hatch = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/hatch/:egg/:hatchingPotion',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -983,9 +819,7 @@ api.hatch = {
|
||||
*/
|
||||
api.equip = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/equip/:type/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1020,9 +854,7 @@ api.equip = {
|
||||
*/
|
||||
api.feed = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/feed/:pet/:food',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1066,9 +898,7 @@ api.feed = {
|
||||
*/
|
||||
api.changeClass = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/change-class',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1089,9 +919,7 @@ api.changeClass = {
|
||||
*/
|
||||
api.disableClasses = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/disable-classes',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1123,9 +951,7 @@ api.disableClasses = {
|
||||
*/
|
||||
api.purchase = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/purchase/:type/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1172,9 +998,7 @@ api.purchase = {
|
||||
*/
|
||||
api.userPurchaseHourglass = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/purchase-hourglass/:type/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1226,9 +1050,7 @@ api.userPurchaseHourglass = {
|
||||
*/
|
||||
api.readCard = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/read-card/:cardType',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1270,9 +1092,7 @@ api.readCard = {
|
||||
*/
|
||||
api.userOpenMysteryItem = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/open-mystery-item',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1304,9 +1124,7 @@ api.userOpenMysteryItem = {
|
||||
*/
|
||||
api.userReleasePets = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/release-pets',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1355,9 +1173,7 @@ api.userReleasePets = {
|
||||
*/
|
||||
api.userReleaseBoth = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/release-both',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1393,9 +1209,7 @@ api.userReleaseBoth = {
|
||||
*/
|
||||
api.userReleaseMounts = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/release-mounts',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1425,9 +1239,7 @@ api.userReleaseMounts = {
|
||||
*/
|
||||
api.userSell = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/sell/:type/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1470,9 +1282,7 @@ api.userSell = {
|
||||
*/
|
||||
api.userUnlock = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/unlock',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1498,9 +1308,7 @@ api.userUnlock = {
|
||||
*/
|
||||
api.userRevive = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/revive',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1510,6 +1318,8 @@ api.userRevive = {
|
||||
},
|
||||
};
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/rebirth Use Orb of Rebirth on user
|
||||
* @apiName UserRebirth
|
||||
@@ -1540,27 +1350,10 @@ api.userRevive = {
|
||||
*/
|
||||
api.userRebirth = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/rebirth',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let tasks = await Tasks.Task.find({
|
||||
userId: user._id,
|
||||
type: {$in: ['daily', 'habit', 'todo']},
|
||||
...Tasks.taskIsGroupOrChallengeQuery,
|
||||
}).exec();
|
||||
|
||||
let rebirthRes = common.ops.rebirth(user, tasks, req, res.analytics);
|
||||
|
||||
let toSave = tasks.map(task => task.save());
|
||||
|
||||
toSave.push(user.save());
|
||||
|
||||
await Promise.all(toSave);
|
||||
|
||||
res.respond(200, ...rebirthRes);
|
||||
await userLib.rebirth(req, res, { isV3: true });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1591,6 +1384,8 @@ api.blockUser = {
|
||||
},
|
||||
};
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {delete} /api/v3/user/messages/:id Delete a message
|
||||
* @apiName deleteMessage
|
||||
@@ -1625,12 +1420,15 @@ api.deleteMessage = {
|
||||
url: '/user/messages/:id',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let deletePMRes = common.ops.deletePM(user, req);
|
||||
await user.save();
|
||||
res.respond(200, ...deletePMRes);
|
||||
|
||||
await inboxLib.deleteMessage(user, req.params.id);
|
||||
|
||||
res.respond(200, ...[await inboxLib.getUserInbox(user, false)]);
|
||||
},
|
||||
};
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {delete} /api/v3/user/messages Delete all messages
|
||||
* @apiName clearMessages
|
||||
@@ -1647,9 +1445,10 @@ api.clearMessages = {
|
||||
url: '/user/messages',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let clearPMsRes = common.ops.clearPMs(user, req);
|
||||
await user.save();
|
||||
res.respond(200, ...clearPMsRes);
|
||||
|
||||
await inboxLib.clearPMs(user);
|
||||
|
||||
res.respond(200, ...[]);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1658,7 +1457,7 @@ api.clearMessages = {
|
||||
* @apiName markPmsRead
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiSuccess {Object} data user.inbox.messages
|
||||
* @apiSuccess {Object} data user.inbox.newMessages
|
||||
*
|
||||
* @apiSuccessExample {json}
|
||||
* {"success":true,"data":[0,"Your private messages have been marked as read"],"notifications":[]}
|
||||
@@ -1676,6 +1475,8 @@ api.markPmsRead = {
|
||||
},
|
||||
};
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/reroll Reroll a user using the Fortify Potion
|
||||
* @apiName UserReroll
|
||||
@@ -1700,29 +1501,15 @@ api.markPmsRead = {
|
||||
*/
|
||||
api.userReroll = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/reroll',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let query = {
|
||||
userId: user._id,
|
||||
type: {$in: ['daily', 'habit', 'todo']},
|
||||
...Tasks.taskIsGroupOrChallengeQuery,
|
||||
};
|
||||
let tasks = await Tasks.Task.find(query).exec();
|
||||
let rerollRes = common.ops.reroll(user, tasks, req, res.analytics);
|
||||
|
||||
let promises = tasks.map(task => task.save());
|
||||
promises.push(user.save());
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
res.respond(200, ...rerollRes);
|
||||
await userLib.reroll(req, res, { isV3: true });
|
||||
},
|
||||
};
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/reset Reset user
|
||||
* @apiName UserReset
|
||||
@@ -1746,32 +1533,10 @@ api.userReroll = {
|
||||
*/
|
||||
api.userReset = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/reset',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
let tasks = await Tasks.Task.find({
|
||||
userId: user._id,
|
||||
...Tasks.taskIsGroupOrChallengeQuery,
|
||||
}).select('_id type challenge group').exec();
|
||||
|
||||
let resetRes = common.ops.reset(user, tasks);
|
||||
|
||||
await Promise.all([
|
||||
Tasks.Task.remove({_id: {$in: resetRes[0].tasksToRemove}, userId: user._id}),
|
||||
user.save(),
|
||||
]);
|
||||
|
||||
res.analytics.track('account reset', {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
});
|
||||
|
||||
res.respond(200, ...resetRes);
|
||||
await userLib.reset(req, res, { isV3: true });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1799,9 +1564,7 @@ api.userReset = {
|
||||
*/
|
||||
api.setCustomDayStart = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/custom-day-start',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1839,9 +1602,7 @@ api.setCustomDayStart = {
|
||||
*/
|
||||
api.togglePinnedItem = {
|
||||
method: 'GET',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/toggle-pinned-item/:type/:path',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -1879,9 +1640,7 @@ api.togglePinnedItem = {
|
||||
api.movePinnedItem = {
|
||||
method: 'POST',
|
||||
url: '/user/move-pinned-item/:path/move/to/:position',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('path', res.t('taskIdRequired')).notEmpty();
|
||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||
|
||||
@@ -1,25 +1,12 @@
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import common from '../../../../common';
|
||||
import {
|
||||
model as Group,
|
||||
} from '../../../models/group';
|
||||
import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../../../libs/errors';
|
||||
import {
|
||||
castTaskSpell,
|
||||
castMultiTaskSpell,
|
||||
castSelfSpell,
|
||||
castPartySpell,
|
||||
castUserSpell,
|
||||
castSpell,
|
||||
} from '../../../libs/spells';
|
||||
import apiError from '../../../libs/apiError';
|
||||
|
||||
const partyMembersFields = 'profile.name stats achievements items.special';
|
||||
|
||||
let api = {};
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target
|
||||
* @apiName UserCast
|
||||
@@ -72,69 +59,9 @@ api.castSpell = {
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/class/cast/:spellId',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let spellId = req.params.spellId;
|
||||
let targetId = req.query.targetId;
|
||||
const quantity = req.body.quantity || 1;
|
||||
|
||||
// optional because not required by all targetTypes, presence is checked later if necessary
|
||||
req.checkQuery('targetId', res.t('targetIdUUID')).optional().isUUID();
|
||||
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class;
|
||||
let spell = common.content.spells[klass][spellId];
|
||||
|
||||
if (!spell) throw new NotFound(apiError('spellNotFound', {spellId}));
|
||||
if (spell.mana > user.stats.mp) throw new NotAuthorized(res.t('notEnoughMana'));
|
||||
if (spell.value > user.stats.gp && !spell.previousPurchase) throw new NotAuthorized(res.t('messageNotEnoughGold'));
|
||||
if (spell.lvl > user.stats.lvl) throw new NotAuthorized(res.t('spellLevelTooHigh', {level: spell.lvl}));
|
||||
|
||||
let targetType = spell.target;
|
||||
|
||||
if (targetType === 'task') {
|
||||
const results = await castTaskSpell(res, req, targetId, user, spell, quantity);
|
||||
res.respond(200, {
|
||||
user: results[0],
|
||||
task: results[1],
|
||||
});
|
||||
} else if (targetType === 'self') {
|
||||
await castSelfSpell(req, user, spell, quantity);
|
||||
res.respond(200, { user });
|
||||
} else if (targetType === 'tasks') { // new target type in v3: when all the user's tasks are necessary
|
||||
const response = await castMultiTaskSpell(req, user, spell, quantity);
|
||||
res.respond(200, response);
|
||||
} else if (targetType === 'party' || targetType === 'user') {
|
||||
const party = await Group.getGroup({groupId: 'party', user});
|
||||
// arrays of users when targetType is 'party' otherwise single users
|
||||
let partyMembers;
|
||||
|
||||
if (targetType === 'party') {
|
||||
partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity);
|
||||
} else {
|
||||
partyMembers = await castUserSpell(res, req, party, partyMembers, targetId, user, spell, quantity);
|
||||
}
|
||||
|
||||
let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers];
|
||||
|
||||
// Only return some fields.
|
||||
// See comment above on why we can't just select the necessary fields when querying
|
||||
partyMembersRes = partyMembersRes.map(partyMember => {
|
||||
return common.pickDeep(partyMember.toJSON(), common.$w(partyMembersFields));
|
||||
});
|
||||
|
||||
res.respond(200, {
|
||||
partyMembers: partyMembersRes,
|
||||
user,
|
||||
});
|
||||
|
||||
if (party && !spell.silent) {
|
||||
let message = `\`${user.profile.name} casts ${spell.text()}${targetType === 'user' ? ` on ${partyMembers.profile.name}` : ' for the party'}.\``;
|
||||
const newChatMessage = party.sendChat(message);
|
||||
await newChatMessage.save();
|
||||
}
|
||||
}
|
||||
await castSpell(req, res, {
|
||||
isV3: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -27,9 +27,7 @@ let api = {};
|
||||
*/
|
||||
api.allocate = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/allocate',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -69,9 +67,7 @@ api.allocate = {
|
||||
*/
|
||||
api.allocateBulk = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/allocate-bulk',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -128,9 +124,7 @@ api.allocateBulk = {
|
||||
*/
|
||||
api.allocateNow = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/allocate-now',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -73,9 +73,7 @@ let api = {};
|
||||
*/
|
||||
api.addWebhook = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/webhook',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -135,9 +133,7 @@ api.addWebhook = {
|
||||
*/
|
||||
api.updateWebhook = {
|
||||
method: 'PUT',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/webhook/:id',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -188,9 +184,7 @@ api.updateWebhook = {
|
||||
*/
|
||||
api.deleteWebhook = {
|
||||
method: 'DELETE',
|
||||
middlewares: [authWithHeaders({
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/webhook/:id',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
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;
|
||||
|
||||
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 { model as User } from '../../models/user';
|
||||
import * as inboxLib from '../../libs/inbox';
|
||||
import * as Tasks from '../../models/task';
|
||||
import {
|
||||
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/...})
|
||||
async function _getUserDataForExport (user, xmlMode = false) {
|
||||
let userData = user.toJSON();
|
||||
userData.tasks = {};
|
||||
|
||||
let tasks = await Tasks.Task.find({
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
userData.inbox.messages = {};
|
||||
|
||||
const [tasks, messages] = await Promise.all([
|
||||
Tasks.Task.find({
|
||||
userId: user._id,
|
||||
}).exec(),
|
||||
|
||||
inboxLib.getUserInbox(user, false),
|
||||
]);
|
||||
|
||||
userData.inbox.messages = messages;
|
||||
|
||||
_(tasks)
|
||||
.map(task => task.toJSON())
|
||||
@@ -296,18 +305,14 @@ api.exportUserPrivateMessages = {
|
||||
url: '/export/inbox.html',
|
||||
middlewares: [authWithSession],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
const user = res.locals.user;
|
||||
|
||||
const timezoneOffset = user.preferences.timezoneOffset;
|
||||
const dateFormat = user.preferences.dateFormat.toUpperCase();
|
||||
const TO = res.t('to');
|
||||
const FROM = res.t('from');
|
||||
|
||||
let inbox = Object.keys(user.inbox.messages).map(key => user.inbox.messages[key]);
|
||||
|
||||
inbox = _.sortBy(inbox, function sortBy (num) {
|
||||
return num.sort * -1;
|
||||
});
|
||||
const inbox = await inboxLib.getUserInbox(user);
|
||||
|
||||
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';
|
||||
|
||||
// @TODO: Don't use this method when the group can be saved.
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { getGroupUrl, sendTxn } from '../email';
|
||||
import slack from '../slack';
|
||||
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';
|
||||
|
||||
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) {
|
||||
if (user.inbox.messages[messageId]) {
|
||||
if (user.inbox.messages[messageId]) { // compatibility
|
||||
delete user.inbox.messages[messageId];
|
||||
user.markModified(`inbox.messages.${messageId}`);
|
||||
await user.save();
|
||||
} 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
NotFound,
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} 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
|
||||
|
||||
@@ -116,4 +124,91 @@ async function castUserSpell (res, req, party, partyMembers, targetId, user, spe
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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 _ from 'lodash';
|
||||
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 validator from 'validator';
|
||||
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},
|
||||
type: {type: String, enum: ['guild', 'party'], required: true},
|
||||
privacy: {type: String, enum: ['private', 'public'], default: 'private', required: true},
|
||||
chat: Array,
|
||||
/*
|
||||
# [{
|
||||
# timestamp: Date
|
||||
# user: String
|
||||
# text: String
|
||||
# contributor: String
|
||||
# uuid: String
|
||||
# id: String
|
||||
# }]
|
||||
*/
|
||||
chat: Array, // Used for backward compatibility, but messages aren't stored here
|
||||
leaderOnly: { // restrict group actions to leader (members can't do them)
|
||||
challenges: {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();
|
||||
};
|
||||
|
||||
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) {
|
||||
let newMessage = chatDefaults(message, user);
|
||||
let newMessage = messageDefaults(message, user);
|
||||
let newChatMessage = new Chat();
|
||||
newChatMessage = Object.assign(newChatMessage, newMessage);
|
||||
newChatMessage.groupId = this._id;
|
||||
@@ -560,17 +481,6 @@ schema.methods.sendChat = function sendChat (message, user, 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
|
||||
if (NO_CHAT_NOTIFICATIONS.indexOf(this._id) !== -1 || this.memberCount > LARGE_GROUP_COUNT_MESSAGE_CUTOFF) {
|
||||
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 {
|
||||
chatDefaults,
|
||||
TAVERN_ID,
|
||||
model as Group,
|
||||
} from '../group';
|
||||
|
||||
import {defaults, map, flatten, flow, compact, uniq, partialRight} from 'lodash';
|
||||
import {model as UserNotification} from '../userNotification';
|
||||
import {
|
||||
messageDefaults,
|
||||
setUserStyles,
|
||||
inboxModel as Inbox,
|
||||
} from '../message';
|
||||
|
||||
import { defaults, map, flatten, flow, compact, uniq, partialRight } from 'lodash';
|
||||
import { model as UserNotification } from '../userNotification';
|
||||
import schema from './schema';
|
||||
import payments from '../../libs/payments/payments';
|
||||
import * as inboxLib from '../../libs/inbox';
|
||||
import amazonPayments from '../../libs/payments/amazon';
|
||||
import stripePayments from '../../libs/payments/stripe';
|
||||
import paypalPayments from '../../libs/payments/paypal';
|
||||
@@ -101,16 +107,19 @@ schema.methods.getObjectionsToInteraction = function getObjectionsToInteraction
|
||||
* @return N/A
|
||||
*/
|
||||
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, options) {
|
||||
let sender = this;
|
||||
let senderMsg = options.senderMsg || options.receiverMsg;
|
||||
const sender = this;
|
||||
const senderMsg = options.senderMsg || options.receiverMsg;
|
||||
// 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._v++;
|
||||
userToReceiveMessage.markModified('inbox.messages');
|
||||
|
||||
/* @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));
|
||||
common.refPush(sender.inbox.messages, newMessage);
|
||||
sender.markModified('inbox.messages');
|
||||
const sendingToYourself = userToReceiveMessage._id === sender._id;
|
||||
|
||||
if (saveUsers) {
|
||||
await Promise.all([userToReceiveMessage.save(), sender.save()]);
|
||||
// Do not add the message twice when sending it to yourself
|
||||
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 () {
|
||||
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],
|
||||
|
||||
inbox: {
|
||||
newMessages: {type: Number, default: 0},
|
||||
blocks: {type: Array, default: () => []},
|
||||
// messages are stored in the Inbox collection, this path will be removed
|
||||
// as soon as the migration has run and all the messages have been removed from here
|
||||
messages: {type: Schema.Types.Mixed, default: () => {
|
||||
return {};
|
||||
}},
|
||||
newMessages: {type: Number, default: 0},
|
||||
blocks: {type: Array, default: () => []},
|
||||
optOut: {type: Boolean, default: false},
|
||||
},
|
||||
tasksOrder: {
|
||||
|
||||
Reference in New Issue
Block a user