Merge pull request #6504 from HabitRPG/api-v3-members
[API v3] Members routes
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"cantDetachFb": "Account lacks another authentication method, can't detach Facebook.",
|
||||
"onlySocialAttachLocal": "Local auth can only be added to a social account.",
|
||||
"invalidReqParams": "Invalid request parameters.",
|
||||
"memberIdRequired": "\"member\" must be a valid UUID.",
|
||||
"taskIdRequired": "\"taskId\" must be a valid UUID.",
|
||||
"taskNotFound": "Task not found.",
|
||||
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
|
||||
@@ -36,6 +37,8 @@
|
||||
"onlyLeaderCanRemoveMember": "Only group leader can remove a member!",
|
||||
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
||||
"groupMemberNotFound": "User not found among group's members",
|
||||
"challengeMemberNotFound": "User not found among challenge's members",
|
||||
"mustBeGroupMember": "Must be member of the group.",
|
||||
"keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
|
||||
"keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
|
||||
"canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /challenges/:challengeId/members', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates optional req.query.lastId to be an UUID', async () => {
|
||||
await expect(user.get(`/challenges/${generateUUID()}/members?lastId=invalidUUID`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if challenge doesn\'t exists', async () => {
|
||||
await expect(user.get(`/challenges/${generateUUID()}/members`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if user doesn\'t have access to the challenge', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await user.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
let anotherUser = await generateUser();
|
||||
await expect(anotherUser.get(`/challenges/${challenge._id}/members`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('works with challenges belonging to public guild', async () => {
|
||||
let leader = await generateUser({balance: 4});
|
||||
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||
let challenge = await leader.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
let res = await user.get(`/challenges/${challenge._id}/members`);
|
||||
expect(res[0]).to.eql({
|
||||
_id: leader._id,
|
||||
profile: {name: leader.profile.name},
|
||||
});
|
||||
expect(res[0]).to.have.all.keys(['_id', 'profile']);
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('populates only some fields', async () => {
|
||||
let anotherUser = await generateUser({balance: 3});
|
||||
let group = await generateGroup(anotherUser, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||
let challenge = await anotherUser.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
let res = await user.get(`/challenges/${challenge._id}/members`);
|
||||
expect(res[0]).to.eql({
|
||||
_id: anotherUser._id,
|
||||
profile: {name: anotherUser.profile.name},
|
||||
});
|
||||
expect(res[0]).to.have.all.keys(['_id', 'profile']);
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('returns only first 30 members', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await user.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 31; i++) {
|
||||
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
let res = await user.get(`/challenges/${challenge._id}/members`);
|
||||
expect(res.length).to.equal(30);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports using req.query.lastId to get more members', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await user.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 57; i++) {
|
||||
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
|
||||
}
|
||||
let generatedUsers = await Promise.all(usersToGenerate); // Group has 59 members (1 is the leader)
|
||||
let expectedIds = [user._id].concat(generatedUsers.map(generatedUser => generatedUser._id));
|
||||
|
||||
let res = await user.get(`/challenges/${challenge._id}/members`);
|
||||
expect(res.length).to.equal(30);
|
||||
let res2 = await user.get(`/challenges/${challenge._id}/members?lastId=${res[res.length - 1]._id}`);
|
||||
expect(res2.length).to.equal(28);
|
||||
|
||||
let resIds = res.concat(res2).map(member => member._id);
|
||||
expect(resIds).to.eql(expectedIds.sort());
|
||||
});
|
||||
});
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /challenges/:challengeId/members/:memberId', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId to be an UUID', async () => {
|
||||
await expect(user.get(`/challenges/invalidUUID/members/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('validates req.params.memberId to be an UUID', async () => {
|
||||
await expect(user.get(`/challenges/${generateUUID()}/members/invalidUUID`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if member doesn\'t exists', async () => {
|
||||
let userId = generateUUID();
|
||||
await expect(user.get(`/challenges/${generateUUID()}/members/${userId}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', {userId}),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if challenge doesn\'t exists', async () => {
|
||||
let member = await generateUser();
|
||||
await expect(user.get(`/challenges/${generateUUID()}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if user doesn\'t have access to the challenge', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await user.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
let anotherUser = await generateUser();
|
||||
let member = await generateUser();
|
||||
await expect(anotherUser.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if member is not part of the challenge', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await user.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
let member = await generateUser();
|
||||
await expect(user.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeMemberNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('works with challenges belonging to a public guild', async () => {
|
||||
let groupLeader = await generateUser({balance: 4});
|
||||
let group = await generateGroup(groupLeader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||
let challenge = await groupLeader.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
let taskText = 'Test Text';
|
||||
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
|
||||
|
||||
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
|
||||
expect(memberProgress).to.have.all.keys(['_id', 'profile', 'tasks']);
|
||||
expect(memberProgress.profile).to.have.all.keys(['name']);
|
||||
expect(memberProgress.tasks.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('returns the member tasks for the challenges', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await user.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: 'Test Text'}]);
|
||||
|
||||
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
|
||||
let chalTasks = await user.get(`/tasks/challenge/${challenge._id}`);
|
||||
expect(memberProgress.tasks.length).to.equal(chalTasks.length);
|
||||
expect(memberProgress.tasks[0].challenge.id).to.equal(challenge._id);
|
||||
expect(memberProgress.tasks[0].challenge.taskId).to.equal(chalTasks[0]._id);
|
||||
});
|
||||
|
||||
it('returns the tasks without the tags', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await user.post('/challenges', {
|
||||
name: 'test chal',
|
||||
shortName: 'test-chal',
|
||||
groupId: group._id,
|
||||
});
|
||||
let taskText = 'Test Text';
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
|
||||
|
||||
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
|
||||
expect(memberProgress.tasks[0]).not.to.have.key('tags');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /groups/:groupId/invites', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates optional req.query.lastId to be an UUID', async () => {
|
||||
await expect(user.get(`/groups/groupId/invites?lastId=invalidUUID`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if group doesn\'t exists', async () => {
|
||||
await expect(user.get(`/groups/${generateUUID()}/invites`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if user doesn\'t have access to the group', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let anotherUser = await generateUser();
|
||||
await expect(anotherUser.get(`/groups/${group._id}/invites`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('works when passing party as req.params.groupId', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let invited = await generateUser();
|
||||
await user.post(`/groups/${group._id}/invite`, {uuids: [invited._id]});
|
||||
let res = await user.get(`/groups/party/invites`);
|
||||
|
||||
expect(res).to.be.an('array');
|
||||
expect(res.length).to.equal(1);
|
||||
expect(res[0]).to.eql({
|
||||
_id: invited._id,
|
||||
profile: {name: invited.profile.name},
|
||||
});
|
||||
});
|
||||
|
||||
it('populates only some fields', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let invited = await generateUser();
|
||||
await user.post(`/groups/${group._id}/invite`, {uuids: [invited._id]});
|
||||
let res = await user.get(`/groups/party/invites`);
|
||||
expect(res[0]).to.have.all.keys(['_id', 'profile']);
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('returns only first 30 invites', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let invitesToGenerate = [];
|
||||
for (let i = 0; i < 31; i++) {
|
||||
invitesToGenerate.push(generateUser());
|
||||
}
|
||||
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||
await user.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
|
||||
|
||||
let res = await user.get(`/groups/party/invites`);
|
||||
expect(res.length).to.equal(30);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports using req.query.lastId to get more invites', async () => {
|
||||
let leader = await generateUser({balance: 4});
|
||||
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||
|
||||
let invitesToGenerate = [];
|
||||
for (let i = 0; i < 32; i++) {
|
||||
invitesToGenerate.push(generateUser());
|
||||
}
|
||||
let generatedInvites = await Promise.all(invitesToGenerate); // Group has 32 invites
|
||||
let expectedIds = generatedInvites.map(generatedInvite => generatedInvite._id);
|
||||
await user.post(`/groups/${group._id}/invite`, {uuids: expectedIds});
|
||||
|
||||
let res = await user.get(`/groups/${group._id}/invites`);
|
||||
expect(res.length).to.equal(30);
|
||||
let res2 = await user.get(`/groups/${group._id}/invites?lastId=${res[res.length - 1]._id}`);
|
||||
expect(res2.length).to.equal(2);
|
||||
|
||||
let resIds = res.concat(res2).map(invite => invite._id);
|
||||
expect(resIds).to.eql(expectedIds.sort());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /groups/:groupId/members', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates optional req.query.lastId to be an UUID', async () => {
|
||||
await expect(user.get(`/groups/groupId/members?lastId=invalidUUID`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if group doesn\'t exists', async () => {
|
||||
await expect(user.get(`/groups/${generateUUID()}/members`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails if user doesn\'t have access to the group', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let anotherUser = await generateUser();
|
||||
await expect(anotherUser.get(`/groups/${group._id}/members`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('works when passing party as req.params.groupId', async () => {
|
||||
await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let res = await user.get(`/groups/party/members`);
|
||||
expect(res).to.be.an('array');
|
||||
expect(res.length).to.equal(1);
|
||||
expect(res[0]).to.eql({
|
||||
_id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
|
||||
it('populates only some fields', async () => {
|
||||
await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let res = await user.get(`/groups/party/members`);
|
||||
expect(res[0]).to.have.all.keys(['_id', 'profile']);
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('returns only first 30 members', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 31; i++) {
|
||||
usersToGenerate.push(generateUser({party: {_id: group._id}}));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
let res = await user.get(`/groups/party/members`);
|
||||
expect(res.length).to.equal(30);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports using req.query.lastId to get more members', async () => {
|
||||
let leader = await generateUser({balance: 4});
|
||||
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 57; i++) {
|
||||
usersToGenerate.push(generateUser({guilds: [group._id]}));
|
||||
}
|
||||
let generatedUsers = await Promise.all(usersToGenerate); // Group has 59 members (1 is the leader)
|
||||
let expectedIds = [leader._id].concat(generatedUsers.map(generatedUser => generatedUser._id));
|
||||
|
||||
let res = await user.get(`/groups/${group._id}/members`);
|
||||
expect(res.length).to.equal(30);
|
||||
let res2 = await user.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`);
|
||||
expect(res2.length).to.equal(28);
|
||||
|
||||
let resIds = res.concat(res2).map(member => member._id);
|
||||
expect(resIds).to.eql(expectedIds.sort());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /members/:memberId', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId', async () => {
|
||||
await expect(user.get(`/members/invalidUUID`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a member public data only', async () => {
|
||||
let member = await generateUser({ // make sure user has all the fields that can be returned by the getMember call
|
||||
contributor: {level: 1},
|
||||
backer: {tier: 3},
|
||||
preferences: {
|
||||
costume: false,
|
||||
background: 'volcano',
|
||||
},
|
||||
});
|
||||
let memberRes = await user.get(`/members/${member._id}`);
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'backer', 'contributor', 'auth', 'items',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
|
||||
'costume', 'sleep', 'background'].sort());
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
let dummyId = generateUUID();
|
||||
await expect(user.get(`/members/${dummyId}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', {userId: dummyId}),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,4 +15,6 @@ describe('GET /tags/:tagId', () => {
|
||||
|
||||
expect(tag).to.deep.equal(createdTag);
|
||||
});
|
||||
|
||||
it('handles non-existing tags');
|
||||
});
|
||||
|
||||
@@ -41,7 +41,6 @@ export async function resetHabiticaDB () {
|
||||
name: 'HabitRPG',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
members: [],
|
||||
}, (insertErr) => {
|
||||
if (insertErr) return reject(insertErr);
|
||||
|
||||
@@ -89,4 +88,3 @@ export function connectToMongo () {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,9 @@ api.createChallenge = {
|
||||
let groupId = req.body.groupId;
|
||||
let prize = req.body.prize;
|
||||
|
||||
let group = await Group.getGroup(user, groupId, '-chat');
|
||||
let group = await Group.getGroup({user, groupId, fields: '-chat', mustBeMember: true});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (!group.isMember(user)) throw new NotAuthorized(res.t('mustBeGroupMember'));
|
||||
|
||||
if (group.leaderOnly && group.leaderOnly.challenges && group.leader !== user._id) {
|
||||
throw new NotAuthorized(res.t('onlyGroupLeaderChal'));
|
||||
@@ -77,9 +78,15 @@ api.createChallenge = {
|
||||
req.body.official = user.contributor.admin && req.body.official;
|
||||
let challenge = new Challenge(Challenge.sanitize(req.body));
|
||||
|
||||
let results = await Q.all(challenge.save(), group.save());
|
||||
let savedChal = results[0];
|
||||
// First validate challenge so we don't save group if it's invalid (only runs sync validators)
|
||||
let challengeValidationErrors = challenge.validateSync();
|
||||
if (challengeValidationErrors) throw challengeValidationErrors;
|
||||
|
||||
let results = await Q.all([challenge.save({
|
||||
validateBeforeSave: false, // already validate
|
||||
}), group.save()]);
|
||||
|
||||
let savedChal = results[0];
|
||||
await savedChal.syncToUser(user); // (it also saves the user)
|
||||
res.respond(201, savedChal);
|
||||
},
|
||||
@@ -100,9 +107,9 @@ api.getChallenges = {
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
let groups = user.guilds || [];
|
||||
let groups = user.guilds.slice(0); // slice is used to clone the array so we don't modify it directly
|
||||
if (user.party._id) groups.push(user.party._id);
|
||||
groups.push('habitrpg'); // Public challenges
|
||||
groups.push('habitrpg'); // tavern challenges
|
||||
|
||||
let challenges = await Challenge.find({
|
||||
$or: [
|
||||
@@ -128,6 +135,8 @@ api.getChallenges = {
|
||||
* @apiName GetChallenge
|
||||
* @apiGroup Challenge
|
||||
*
|
||||
* @apiParam {UUID} challengeId The challenge _id
|
||||
*
|
||||
* @apiSuccess {object} challenge The challenge object
|
||||
*/
|
||||
api.getChallenge = {
|
||||
@@ -140,16 +149,14 @@ api.getChallenge = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let user = res.local.user;
|
||||
let user = res.locals.user;
|
||||
let challengeId = req.params.challengeId;
|
||||
|
||||
let challenge = await Challenge.findOne({_id: challengeId}).exec(); // TODO populate
|
||||
let challenge = await Challenge.findById(challengeId).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
// If the challenge does not exist, or if it exists but user is not a member, not the leader and not an admin -> throw error
|
||||
// TODO support challenges in groups I'm a member of
|
||||
if (!challenge || (user.challenges.indexOf(challengeId) === -1 && challenge.leader !== user._id && !user.contributor.admin)) { // eslint-disable-line no-extra-parens
|
||||
throw new NotFound(res.t('challengeNotFound'));
|
||||
}
|
||||
let group = await Group.getGroup({user, groupId: challenge.groupId, fields: '_id type privacy'});
|
||||
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
res.respond(200, challenge);
|
||||
},
|
||||
@@ -168,7 +175,7 @@ function _closeChal (challenge, broken = {}) {
|
||||
Tasks.Task.remove({'challenge.id': challenge._id, userId: {$exists: false}}).exec(),
|
||||
// Set the challenge tag to non-challenge status and remove the challenge from the user's challenges
|
||||
User.update({
|
||||
challenges: {$in: [challenge._id]},
|
||||
challenges: challenge._id,
|
||||
'tags._id': challenge._id,
|
||||
}, {
|
||||
$set: {'tags.$.challenge': false},
|
||||
@@ -233,7 +240,7 @@ api.deleteChallenge = {
|
||||
|
||||
let challenge = await Challenge.findOne({_id: req.params.challengeId}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id && !user.contributor.admin) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
||||
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
||||
|
||||
res.respond(200, {});
|
||||
// Close channel in background
|
||||
@@ -264,7 +271,7 @@ api.selectChallengeWinner = {
|
||||
|
||||
let challenge = await Challenge.findOne({_id: req.params.challengeId}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id && !user.contributor.admin) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
||||
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
||||
|
||||
let winner = await User.findOne({_id: req.params.winnerId}).exec();
|
||||
if (!winner || winner.challenges.indexOf(challenge._id) === -1) throw new NotFound(res.t('winnerNotFound', {userId: req.parama.winnerId}));
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '../../libs/api-v3/errors';
|
||||
import _ from 'lodash';
|
||||
import { sendTxn } from '../../libs/api-v3/email';
|
||||
import nconf from 'nconf';
|
||||
import nconf from 'nconf';
|
||||
|
||||
let api = {};
|
||||
|
||||
@@ -33,7 +33,7 @@ api.getChat = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, req.params.groupId, 'chat');
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: 'chat'});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
res.respond(200, group.chat);
|
||||
@@ -67,7 +67,7 @@ api.postChat = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, groupId);
|
||||
let group = await Group.getGroup({user, groupId});
|
||||
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.type !== 'party' && user.flags.chatRevoked) {
|
||||
@@ -118,7 +118,7 @@ api.likeChat = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, groupId);
|
||||
let group = await Group.getGroup({user, groupId});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
let message = _.find(group.chat, {id: req.params.chatId});
|
||||
@@ -165,7 +165,7 @@ api.flagChat = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, groupId);
|
||||
let group = await Group.getGroup({user, groupId});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
let message = _.find(group.chat, {id: req.params.chatId});
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ api.getGroups = {
|
||||
types.forEach(type => {
|
||||
switch (type) {
|
||||
case 'party':
|
||||
queries.push(Group.getGroup(user, 'party', groupFields));
|
||||
queries.push(Group.getGroup({user, groupId: 'party', fields: groupFields, populateLeader: true}));
|
||||
break;
|
||||
case 'privateGuilds':
|
||||
queries.push(Group.find({
|
||||
@@ -109,7 +109,7 @@ api.getGroups = {
|
||||
}).select(groupFields).sort(sort).exec()); // TODO use lean?
|
||||
break;
|
||||
case 'tavern':
|
||||
queries.push(Group.getGroup(user, 'habitrpg', groupFields));
|
||||
queries.push(Group.getGroup({user, groupId: 'habitrpg', fields: groupFields, populateLeader: true}));
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -149,7 +149,7 @@ api.getGroup = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, req.params.groupId);
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, populateLeader: true});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
res.respond(200, group);
|
||||
@@ -178,7 +178,7 @@ api.updateGroup = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, req.params.groupId);
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
|
||||
@@ -214,7 +214,8 @@ api.joinGroup = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, req.params.groupId, '-chat', true); // Do not fetch chat and work even if the user is not yet a member of the group
|
||||
// Do not fetch chat and work even if the user is not yet a member of the group
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat', optionalMembership: true}); // Do not fetch chat and work even if the user is not yet a member of the group
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
let isUserInvited = false;
|
||||
@@ -288,7 +289,7 @@ api.leaveGroup = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, req.params.groupId, '-chat'); // Do not fetch chat
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat'}); // Do not fetch chat
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
// During quests, checke wheter user can leave
|
||||
@@ -344,7 +345,7 @@ api.removeGroupMember = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, req.params.groupId, '-chat'); // Do not fetch chat
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat'}); // Do not fetch chat
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
let uuid = req.query.memberId;
|
||||
@@ -523,7 +524,7 @@ api.inviteToGroup = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let group = await Group.getGroup(user, req.params.groupId, '-chat'); // Do not fetch chat TODO other fields too?
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat'}); // Do not fetch chat TODO other fields too?
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
let uuids = req.body.uuids;
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import { authWithHeaders } from '../../middlewares/api-v3/auth';
|
||||
import cron from '../../middlewares/api-v3/cron';
|
||||
import {
|
||||
model as User,
|
||||
publicFields as memberFields,
|
||||
nameFields,
|
||||
} from '../../models/user';
|
||||
import { model as Group } from '../../models/group';
|
||||
import { model as Challenge } from '../../models/challenge';
|
||||
import {
|
||||
NotFound,
|
||||
} from '../../libs/api-v3/errors';
|
||||
import * as Tasks from '../../models/task';
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @api {get} /members/:memberId Get a member profile
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetMember
|
||||
* @apiGroup Member
|
||||
*
|
||||
* @apiParam {UUID} memberId The member's id
|
||||
*
|
||||
* @apiSuccess {object} member The member object
|
||||
*/
|
||||
api.getMember = {
|
||||
method: 'GET',
|
||||
url: '/members/:memberId',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let memberId = req.params.memberId;
|
||||
|
||||
let member = await User
|
||||
.findById(memberId)
|
||||
.select(memberFields)
|
||||
.exec();
|
||||
|
||||
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
||||
|
||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||
res.respond(200, member.toJSON({minimize: true}));
|
||||
},
|
||||
};
|
||||
|
||||
// Return a request handler for getMembersForGroup / getInvitesForGroup / getMembersForChallenge
|
||||
// type is `invites` or `members`
|
||||
function _getMembersForItem (type) {
|
||||
if (['group-members', 'group-invites', 'challenge-members'].indexOf(type) === -1) {
|
||||
throw new Error('Type must be one of "group-members", "group-invites", "challenge-members"');
|
||||
}
|
||||
|
||||
return async function handleGetMembersForItem (req, res) {
|
||||
if (type === 'challenge-members') {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
} else {
|
||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty();
|
||||
}
|
||||
req.checkQuery('lastId').optional().notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let groupId = req.params.groupId;
|
||||
let challengeId = req.params.challengeId;
|
||||
let lastId = req.query.lastId;
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
let group;
|
||||
|
||||
if (type === 'challenge-members') {
|
||||
challenge = await Challenge.findById(challengeId).select('_id type leader groupId').exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
group = await Group.getGroup({user, groupId: challenge.groupId, fields: '_id type privacy'});
|
||||
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
|
||||
} else {
|
||||
group = await Group.getGroup({user, groupId, fields: '_id type'});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
}
|
||||
|
||||
let query = {};
|
||||
let fields = nameFields;
|
||||
|
||||
if (type === 'challenge-members') {
|
||||
query.challenges = challenge._id;
|
||||
} else if (type === 'group-members') {
|
||||
if (group.type === 'guild') {
|
||||
query.guilds = group._id;
|
||||
} else {
|
||||
query['party._id'] = group._id; // group._id and not groupId because groupId could be === 'party'
|
||||
|
||||
if (req.query.includeAllPublicFields === 'true') {
|
||||
fields = memberFields;
|
||||
}
|
||||
}
|
||||
} else if (type === 'group-invites') {
|
||||
if (group.type === 'guild') { // eslint-disable-line no-lonely-if
|
||||
query['invitations.guilds.id'] = group._id;
|
||||
} else {
|
||||
query['invitations.party.id'] = group._id; // group._id and not groupId because groupId could be === 'party'
|
||||
}
|
||||
}
|
||||
|
||||
if (lastId) query._id = {$gt: lastId};
|
||||
|
||||
let members = await User
|
||||
.find(query)
|
||||
.sort({_id: 1})
|
||||
.limit(30)
|
||||
.select(fields)
|
||||
.exec();
|
||||
|
||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||
res.respond(200, members.map(member => member.toJSON({minimize: true})));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /groups/:groupId/members Get members for a group with a limit of 30 member per request. To get all members run requests against this routes (updating the lastId query parameter) until you get less than 30 results.
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetMembersForGroup
|
||||
* @apiGroup Member
|
||||
*
|
||||
* @apiParam {UUID} groupId The group id
|
||||
* @apiParam {UUID} lastId Query parameter to specify the last member returned in a previous request to this route and get the next batch of results
|
||||
* @apiParam {boolean} includeAllPublicFields Query parameter avalaible only when fetching a party. If === `true` then all public fields for members will be returned (liek when making a request for a single member)
|
||||
*
|
||||
* @apiSuccess {array} members An array of members, sorted by _id
|
||||
*/
|
||||
api.getMembersForGroup = {
|
||||
method: 'GET',
|
||||
url: '/groups/:groupId/members',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
handler: _getMembersForItem('group-members'),
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /groups/:groupId/invites Get invites for a group with a limit of 30 member per request. To get all invites run requests against this routes (updating the lastId query parameter) until you get less than 30 results.
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetInvitesForGroup
|
||||
* @apiGroup Member
|
||||
*
|
||||
* @apiParam {UUID} groupId The group id
|
||||
* @apiParam {UUID} lastId Query parameter to specify the last invite returned in a previous request to this route and get the next batch of results
|
||||
*
|
||||
* @apiSuccess {array} invites An array of invites, sorted by _id
|
||||
*/
|
||||
api.getInvitesForGroup = {
|
||||
method: 'GET',
|
||||
url: '/groups/:groupId/invites',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
handler: _getMembersForItem('group-invites'),
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /challenges/:challengeId/members Get members for a challenge with a limit of 30 member per request. To get all members run requests against this routes (updating the lastId query parameter) until you get less than 30 results.
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetMembersForChallenge
|
||||
* @apiGroup Member
|
||||
*
|
||||
* @apiParam {UUID} challengeId The challenge id
|
||||
* @apiParam {UUID} lastId Query parameter to specify the last member returned in a previous request to this route and get the next batch of results
|
||||
*
|
||||
* @apiSuccess {array} members An array of members, sorted by _id
|
||||
*/
|
||||
api.getMembersForChallenge = {
|
||||
method: 'GET',
|
||||
url: '/challenges/:challengeId/members',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
handler: _getMembersForItem('challenge-members'),
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /challenges/:challengeId/members/:memberId Get a challenge member progress
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetChallenge
|
||||
* @apiGroup Challenge
|
||||
*
|
||||
* @apiParam {UUID} challengeId The challenge _id
|
||||
* @apiParam {UUID} member The member _id
|
||||
*
|
||||
* @apiSuccess {object} member Return an object with member _id, profile.name and a tasks object with the challenge tasks for the member
|
||||
*/
|
||||
api.getChallengeMemberProgress = {
|
||||
method: 'GET',
|
||||
url: '/challenges/:challengeId/members/:memberId',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let user = res.locals.user;
|
||||
let challengeId = req.params.challengeId;
|
||||
let memberId = req.params.memberId;
|
||||
|
||||
let member = await User.findById(memberId).select(`${nameFields} challenges`).exec();
|
||||
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
||||
|
||||
let challenge = await Challenge.findById(challengeId).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
let group = await Group.getGroup({user, groupId: challenge.groupId, fields: '_id type privacy'});
|
||||
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (!challenge.isMember(member)) throw new NotFound(res.t('challengeMemberNotFound'));
|
||||
|
||||
let chalTasks = await Tasks.Task.find({
|
||||
userId: memberId,
|
||||
'challenge.id': challengeId,
|
||||
})
|
||||
.select('-tags') // We don't want to return the tags publicly TODO same for other data?
|
||||
.exec();
|
||||
|
||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||
let response = member.toJSON({minimize: true});
|
||||
delete response.challenges;
|
||||
response.tasks = chalTasks.map(chalTask => chalTask.toJSON({minimize: true}));
|
||||
res.respond(200, response);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
@@ -84,15 +84,15 @@ api.createUserTasks = {
|
||||
*/
|
||||
api.createChallengeTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks/challenge/:challengeId',
|
||||
url: '/tasks/challenge/:challengeId', // TODO should be /tasks/challengeS/:challengeId ? plural?
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let user = res.local.user;
|
||||
let user = res.locals.user;
|
||||
let challengeId = req.params.challengeId;
|
||||
|
||||
let challenge = await Challenge.findOne({_id: challengeId}).exec();
|
||||
@@ -188,13 +188,13 @@ api.getChallengeTasks = {
|
||||
url: '/tasks/challenge/:challengeId',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(Tasks.tasksTypes);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let user = res.local.user;
|
||||
let user = res.locals.user;
|
||||
let challengeId = req.params.challengeId;
|
||||
|
||||
let challenge = await Challenge.findOne({_id: challengeId}).select('leader').exec();
|
||||
|
||||
@@ -20,10 +20,9 @@ let schema = new Schema({
|
||||
rewards: [{type: String, ref: 'Task'}],
|
||||
},
|
||||
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
||||
groupId: {type: String, ref: 'Group', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
||||
groupId: {type: String, ref: 'Group', validate: [validator.isUUID, 'Invalid uuid.'], required: true}, // TODO no update, no set?
|
||||
timestamp: {type: Date, default: Date.now, required: true}, // TODO what is this? use timestamps from plugin? not settable?
|
||||
memberCount: {type: Number, default: 0},
|
||||
challengeCount: {type: Number, default: 0},
|
||||
prize: {type: Number, default: 0, min: 0}, // TODO no update?
|
||||
});
|
||||
|
||||
@@ -31,11 +30,37 @@ schema.plugin(baseModel, {
|
||||
noSet: ['_id', 'memberCount', 'challengeCount', 'tasksOrder'],
|
||||
});
|
||||
|
||||
// Returns true if user has access to the challenge (can join)
|
||||
schema.methods.hasAccess = function hasAccessToChallenge (user) {
|
||||
let userGroups = user.guilds.slice(0);
|
||||
if (user.party._id) userGroups.push(user.party._id);
|
||||
userGroups.push('habitrpg'); // tavern challenges
|
||||
return this.leader === user._id || userGroups.indexOf(this.groupId) !== -1;
|
||||
};
|
||||
|
||||
// Returns true if user can view the challenge
|
||||
// Different from hasAccess because challenges of public guilds can be viewed by everyone
|
||||
schema.methods.canView = function canViewChallenge (user, group) {
|
||||
if (user.contributor.admin) return true;
|
||||
if (group.type === 'guild' && group.privacy === 'public') return true;
|
||||
return this.hasAccess(user);
|
||||
};
|
||||
|
||||
// Returns true if user is a member of the challenge
|
||||
schema.methods.isMember = function isChallengeMember (user) {
|
||||
return user.challenges.indexOf(this._id) !== -1;
|
||||
};
|
||||
|
||||
// Returns true if the user can modify (close, selectWinner, ...) the challenge
|
||||
schema.methods.canModify = function canModifyChallenge (user) {
|
||||
return user.contributor.admin || this.leader === user._id;
|
||||
};
|
||||
|
||||
// Takes a Task document and return a plain object of attributes that can be synced to the user
|
||||
function _syncableAttrs (task) {
|
||||
let t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
||||
// only sync/compare important attrs
|
||||
let omitAttrs = ['userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO what to do with updatedAt?
|
||||
let omitAttrs = ['_id', 'userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO what to do with updatedAt?
|
||||
if (t.type !== 'reward') omitAttrs.push('value');
|
||||
return _.omit(t, omitAttrs);
|
||||
}
|
||||
@@ -143,7 +168,7 @@ schema.methods.addTasks = async function challengeAddTasks (tasks) {
|
||||
tasksOrderList.$each.unshift(userTask._id);
|
||||
}
|
||||
|
||||
toSave.push(userTask);
|
||||
toSave.push(userTask.save());
|
||||
});
|
||||
|
||||
// Update the user
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { model as User} from './user';
|
||||
import {
|
||||
model as User,
|
||||
nameFields,
|
||||
} from './user';
|
||||
import shared from '../../../common';
|
||||
import _ from 'lodash';
|
||||
import { model as Challenge} from './challenge';
|
||||
@@ -40,7 +43,6 @@ export let schema = new Schema({
|
||||
balance: {type: Number, default: 0},
|
||||
logo: String,
|
||||
leaderMessage: String,
|
||||
// challenges: [{type: String, validate: [validator.isUUID, 'Invalid uuid.'], ref: 'Challenge'}], // TODO do we need this? could depend on back-ref instead (Challenge.find({group:GID}))
|
||||
quest: {
|
||||
key: String,
|
||||
active: {type: Boolean, default: false},
|
||||
@@ -57,6 +59,7 @@ export let schema = new Schema({
|
||||
// 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them.
|
||||
// TODO when booting user, remove from .joined and check again if we can now start the quest
|
||||
// TODO as long as quests are party only we can keep it here
|
||||
// TODO are we sure we need this type of default for this to work?
|
||||
members: {type: Schema.Types.Mixed, default: () => {
|
||||
return {};
|
||||
}},
|
||||
@@ -125,8 +128,8 @@ schema.post('remove', function postRemoveGroup (group) {
|
||||
firebase.deleteGroup(group._id);
|
||||
});
|
||||
|
||||
// TODO populate (invites too), isMember?
|
||||
schema.statics.getGroup = function getGroup (user, groupId, fields, optionalMembership) {
|
||||
schema.statics.getGroup = function getGroup (options = {}) {
|
||||
let {user, groupId, fields, optionalMembership = false, populateLeader = false} = options;
|
||||
let query;
|
||||
|
||||
// When optionalMembership is true it's not required for the user to be a member of the group
|
||||
@@ -142,10 +145,22 @@ schema.statics.getGroup = function getGroup (user, groupId, fields, optionalMemb
|
||||
|
||||
let mQuery = this.findOne(query);
|
||||
if (fields) mQuery.select(fields);
|
||||
return mQuery.exec(); // TODO catch errors here?
|
||||
if (populateLeader === true) mQuery.populate('leader', nameFields);
|
||||
return mQuery.exec();
|
||||
// TODO purge chat flags info? in tojson?
|
||||
};
|
||||
|
||||
// Return true if user is a member of the group
|
||||
schema.methods.isMember = function isGroupMember (user) {
|
||||
if (this._id === 'habitrpg') {
|
||||
return true; // everyone is considered part of the tavern
|
||||
} else if (this.type === 'party') {
|
||||
return user.party._id === this._id ? true : false;
|
||||
} else { // guilds
|
||||
return user.guilds.indexOf(this._id) !== -1;
|
||||
}
|
||||
};
|
||||
|
||||
export function chatDefaults (msg, user) {
|
||||
let message = {
|
||||
id: shared.uuid(),
|
||||
@@ -504,6 +519,7 @@ schema.methods.leave = function leaveGroup (user, keep) {
|
||||
});
|
||||
};
|
||||
|
||||
export const INVITES_LIMIT = 100;
|
||||
export let model = mongoose.model('Group', schema);
|
||||
|
||||
// initialize tavern if !exists (fresh installs)
|
||||
@@ -512,12 +528,12 @@ model.count({_id: 'habitrpg'}, (err, ct) => {
|
||||
if (ct > 0) return;
|
||||
|
||||
new model({ // eslint-disable-line babel/new-cap
|
||||
_id: 'habitrpg', // TODO hmm this will probably break everything
|
||||
_id: 'habitrpg',
|
||||
leader: '9', // TODO change this user id
|
||||
name: 'HabitRPG',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
}).save();
|
||||
}).save({
|
||||
validateBeforeSave: false, // _id = 'habitrpg' would not be valid otherwise
|
||||
}); // TODO catch/log?
|
||||
});
|
||||
|
||||
export const INVITES_LIMIT = 100;
|
||||
|
||||
@@ -338,7 +338,6 @@ export let schema = new Schema({
|
||||
orderAscending: {type: String, default: 'ascending'},
|
||||
quest: {
|
||||
key: String,
|
||||
// TODO why are we storing quest progress here too and not only on party object?
|
||||
progress: {
|
||||
up: {type: Number, default: 0},
|
||||
down: {type: Number, default: 0},
|
||||
@@ -473,23 +472,34 @@ export let schema = new Schema({
|
||||
},
|
||||
}, {
|
||||
strict: true,
|
||||
minimize: false, // So empty objects are returned
|
||||
minimize: false, // So empty objects are returned TODO make sure it's in every model
|
||||
});
|
||||
|
||||
schema.plugin(baseModel, {
|
||||
// TODO revisit a lot of things are missing
|
||||
noSet: ['_id', 'apiToken', 'auth.blocked', 'auth.timestamps', 'lastCron', 'auth.local.hashed_password', 'auth.local.salt', 'tasksOrder', 'tags', 'stats', 'challenges', 'guilds', 'party._id', 'party.quest', 'invitations', 'balance'],
|
||||
// TODO revisit a lot of things are missing. Given how many attributes we do have here we should white-list the ones that can be updated
|
||||
noSet: ['_id', 'apiToken', 'auth.blocked', 'auth.timestamps', 'lastCron', 'auth.local.hashed_password',
|
||||
'auth.local.salt', 'tasksOrder', 'tags', 'stats', 'challenges', 'guilds', 'party._id', 'party.quest',
|
||||
'invitations', 'balance', 'backer', 'contributor'],
|
||||
private: ['auth.local.hashed_password', 'auth.local.salt'],
|
||||
toJSONTransform: function userToJSON (doc) {
|
||||
// FIXME? Is this a reference to `doc.filters` or just disabled code? Remove?
|
||||
// TODO this works?
|
||||
doc.filters = {};
|
||||
doc._tmp = this._tmp; // be sure to send down drop notifs
|
||||
// doc.filters = {};
|
||||
// doc._tmp = this._tmp; // be sure to send down drop notifs
|
||||
|
||||
return doc;
|
||||
},
|
||||
});
|
||||
|
||||
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private)
|
||||
// TODO is all party data meant to be public?
|
||||
export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
|
||||
preferences.costume preferences.sleep preferences.background profile stats achievements party
|
||||
backer contributor auth.timestamps items`;
|
||||
|
||||
// The minimum amount of data needed when populating multiple users
|
||||
export let nameFields = `profile.name`;
|
||||
|
||||
schema.post('init', function postInitUser (doc) {
|
||||
shared.wrap(doc);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user