diff --git a/package-lock.json b/package-lock.json index f9147905b1..106bc559f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2347,6 +2347,19 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "optional": true }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -6259,6 +6272,22 @@ "optional": true, "requires": { "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "yallist": { @@ -6813,6 +6842,22 @@ "requires": { "glob": "^7.0.3", "minimatch": "^3.0.3" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "fill-range": { @@ -6928,6 +6973,21 @@ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "requires": { "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } } } @@ -7413,16 +7473,34 @@ "dev": true }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz", + "integrity": "sha512-cF7FYZZ47YzmCu7dDy50xSRRfO3ErRfrXuLZcNIuyiJEco0XSrGtuilG19L5xp3NcwTx7Gn+X6Tv3fmsUPTbow==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^5.0.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "glob-parent": { @@ -7450,6 +7528,19 @@ "unique-stream": "^2.0.2" }, "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -7744,6 +7835,19 @@ "slash": "^3.0.0" }, "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, "ignore": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", @@ -9219,6 +9323,22 @@ "dev": true, "requires": { "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } } } @@ -10534,6 +10654,22 @@ "dev": true, "requires": { "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } } } @@ -12708,6 +12844,21 @@ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { "glob": "^7.1.3" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "run-async": { diff --git a/package.json b/package.json index 91f189b711..4cdfd2d4dc 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "express": "^4.17.3", "express-basic-auth": "^1.2.1", "express-validator": "^5.2.0", - "glob": "^7.2.0", + "glob": "^8.0.1", "got": "^11.8.3", "gulp": "^4.0.0", "gulp-babel": "^8.0.0", diff --git a/test/api/unit/libs/items/utils.test.js b/test/api/unit/libs/items/utils.test.js index 6a0e7ce34d..d1bf1f126e 100644 --- a/test/api/unit/libs/items/utils.test.js +++ b/test/api/unit/libs/items/utils.test.js @@ -99,23 +99,26 @@ describe('Items Utils', () => { expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5); }); - it('converts values for mounts paths to numbers', () => { - expect(castItemVal('items.mounts.Cactus-Base', 'true')).to.equal(true); - expect(castItemVal('items.mounts.Aether-Invisible', 'false')).to.equal(false); - expect(castItemVal('items.mounts.Aether-Invalid', 'true')).to.equal(true); - expect(castItemVal('items.mounts.Aether-Invalid', 'truish')).to.equal(true); - expect(castItemVal('items.mounts.Aether-Invalid', 0)).to.equal(false); - }); - it('converts values for quests paths to numbers', () => { expect(castItemVal('items.quests.atom3', '5')).to.equal(5); expect(castItemVal('items.quests.invalid', '5')).to.equal(5); }); - it('converts values for owned gear', () => { + it('converts values for mounts paths to true/null', () => { + // mounts are never false but can be null (function contains more details) + expect(castItemVal('items.mounts.Cactus-Base', 'true')).to.equal(true); + expect(castItemVal('items.mounts.Aether-Invisible', 'null')).to.equal(null); + expect(castItemVal('items.mounts.Aether-Invisible', 'false')).to.equal(null); + expect(castItemVal('items.mounts.Aether-Invalid', 'true')).to.equal(true); + expect(castItemVal('items.mounts.Aether-Invalid', 'truthy')).to.equal(true); + expect(castItemVal('items.mounts.Aether-Invalid', 0)).to.equal(null); + }); + + it('converts values for owned gear to true/false', () => { expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true); expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false); - expect(castItemVal('items.gear.owned.invalid', 'thruthy')).to.equal(true); + expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false); + expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true); expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false); }); }); diff --git a/test/api/unit/middlewares/ensureAccessRight.test.js b/test/api/unit/middlewares/ensureAccessRight.test.js index d163ae704d..d87cb6d035 100644 --- a/test/api/unit/middlewares/ensureAccessRight.test.js +++ b/test/api/unit/middlewares/ensureAccessRight.test.js @@ -4,8 +4,7 @@ import { generateReq, generateNext, } from '../../../helpers/api-unit.helper'; -import i18n from '../../../../website/common/script/i18n'; -import { ensureAdmin, ensureSudo, ensureNewsPoster } from '../../../../website/server/middlewares/ensureAccessRight'; +import { ensurePermission } from '../../../../website/server/middlewares/ensureAccessRight'; import { NotAuthorized } from '../../../../website/server/libs/errors'; import apiError from '../../../../website/server/libs/apiError'; @@ -20,20 +19,20 @@ describe('ensure access middlewares', () => { }); context('ensure admin', () => { - it('returns not authorized when user is not an admin', () => { - res.locals = { user: { contributor: { admin: false } } }; + it('returns not authorized when user is not in userSupport', () => { + res.locals = { user: { permissions: { userSupport: false } } }; - ensureAdmin(req, res, next); + ensurePermission('userSupport')(req, res, next); const calledWith = next.getCall(0).args; - expect(calledWith[0].message).to.equal(i18n.t('noAdminAccess')); + expect(calledWith[0].message).to.equal(apiError('noPrivAccess')); expect(calledWith[0] instanceof NotAuthorized).to.equal(true); }); - it('passes when user is an admin', () => { - res.locals = { user: { contributor: { admin: true } } }; + it('passes when user is an userSuppor', () => { + res.locals = { user: { permissions: { userSupport: true } } }; - ensureAdmin(req, res, next); + ensurePermission('userSupport')(req, res, next); expect(next).to.be.calledOnce; expect(next.args[0]).to.be.empty; @@ -42,40 +41,40 @@ describe('ensure access middlewares', () => { context('ensure newsPoster', () => { it('returns not authorized when user is not a newsPoster', () => { - res.locals = { user: { contributor: { newsPoster: false } } }; + res.locals = { user: { permissions: { news: false } } }; - ensureNewsPoster(req, res, next); + ensurePermission('news')(req, res, next); const calledWith = next.getCall(0).args; - expect(calledWith[0].message).to.equal(apiError('noNewsPosterAccess')); + expect(calledWith[0].message).to.equal(apiError('noPrivAccess')); expect(calledWith[0] instanceof NotAuthorized).to.equal(true); }); it('passes when user is a newsPoster', () => { - res.locals = { user: { contributor: { newsPoster: true } } }; + res.locals = { user: { permissions: { news: true } } }; - ensureNewsPoster(req, res, next); + ensurePermission('news')(req, res, next); expect(next).to.be.calledOnce; expect(next.args[0]).to.be.empty; }); }); - context('ensure sudo', () => { - it('returns not authorized when user is not a sudo user', () => { - res.locals = { user: { contributor: { sudo: false } } }; + context('ensure coupons', () => { + it('returns not authorized when user does not have access to coupon calls', () => { + res.locals = { user: { permissions: { coupons: false } } }; - ensureSudo(req, res, next); + ensurePermission('coupons')(req, res, next); const calledWith = next.getCall(0).args; - expect(calledWith[0].message).to.equal(apiError('noSudoAccess')); + expect(calledWith[0].message).to.equal(apiError('noPrivAccess')); expect(calledWith[0] instanceof NotAuthorized).to.equal(true); }); - it('passes when user is a sudo user', () => { - res.locals = { user: { contributor: { sudo: true } } }; + it('passes when user has access to coupon calls', () => { + res.locals = { user: { permissions: { coupons: true } } }; - ensureSudo(req, res, next); + ensurePermission('coupons')(req, res, next); expect(next).to.be.calledOnce; expect(next.args[0]).to.be.empty; diff --git a/test/api/unit/models/group.test.js b/test/api/unit/models/group.test.js index 59efba1134..fd5cea9b05 100644 --- a/test/api/unit/models/group.test.js +++ b/test/api/unit/models/group.test.js @@ -1029,7 +1029,7 @@ describe('Group Model', () => { expect(toJSON.chat.length).to.equal(1); }); - it('shows messages with >= 2 flag to admins', async () => { + it('shows messages with >= 2 flag to moderators', async () => { party.chat = [{ flagCount: 3, info: { @@ -1037,12 +1037,12 @@ describe('Group Model', () => { quest: 'basilist', }, }]; - const admin = new User({ 'contributor.admin': true }); + const admin = new User({ 'permissions.moderator': true }); const toJSON = await Group.toJSONCleanChat(party, admin); expect(toJSON.chat.length).to.equal(1); }); - it('doesn\'t show flagged messages to non-admins', async () => { + it('doesn\'t show flagged messages to non-moderators', async () => { party.chat = [{ flagCount: 3, info: { diff --git a/test/api/unit/models/user.test.js b/test/api/unit/models/user.test.js index d559ba0025..06307e39a3 100644 --- a/test/api/unit/models/user.test.js +++ b/test/api/unit/models/user.test.js @@ -877,7 +877,7 @@ describe('User Model', () => { expect(user.isNewsPoster()).to.equal(false); - user.contributor.newsPoster = true; + user.permissions = { news: true }; expect(user.isNewsPoster()).to.equal(true); }); diff --git a/test/api/v3/integration/challenges/GET-challenges_group_groupid.test.js b/test/api/v3/integration/challenges/GET-challenges_group_groupid.test.js index 8e4fc6a432..fc26d4ce0d 100644 --- a/test/api/v3/integration/challenges/GET-challenges_group_groupid.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_group_groupid.test.js @@ -202,7 +202,7 @@ describe('GET challenges/groups/:groupId', () => { publicGuild = group; await user.update({ - 'contributor.admin': true, + 'permissions.challengeAdmin': true, }); officialChallenge = await generateChallenge(user, group, { diff --git a/test/api/v3/integration/challenges/GET-challenges_user.test.js b/test/api/v3/integration/challenges/GET-challenges_user.test.js index 0ff9385891..cc9c3aabd7 100644 --- a/test/api/v3/integration/challenges/GET-challenges_user.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_user.test.js @@ -231,7 +231,7 @@ describe('GET challenges/user', () => { publicGuild = group; await user.update({ - 'contributor.admin': true, + 'permissions.challengeAdmin': true, }); officialChallenge = await generateChallenge(user, group, { diff --git a/test/api/v3/integration/challenges/POST-challenges.test.js b/test/api/v3/integration/challenges/POST-challenges.test.js index 8896c3e79d..e1b3dc63a0 100644 --- a/test/api/v3/integration/challenges/POST-challenges.test.js +++ b/test/api/v3/integration/challenges/POST-challenges.test.js @@ -203,8 +203,8 @@ describe('POST /challenges', () => { it('sets challenge as official if created by admin and official flag is set', async () => { await groupLeader.update({ - contributor: { - admin: true, + permissions: { + challengeAdmin: true, }, }); diff --git a/test/api/v3/integration/chat/DELETE-chat_id.test.js b/test/api/v3/integration/chat/DELETE-chat_id.test.js index 58203ea94d..0d53dfa999 100644 --- a/test/api/v3/integration/chat/DELETE-chat_id.test.js +++ b/test/api/v3/integration/chat/DELETE-chat_id.test.js @@ -22,7 +22,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => { message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' }); message = message.message; userThatDidNotCreateChat = await generateUser(); - admin = await generateUser({ 'contributor.admin': true }); + admin = await generateUser({ 'permissions.moderator': true }); }); context('Chat errors', () => { diff --git a/test/api/v3/integration/chat/POST-chat.flag.test.js b/test/api/v3/integration/chat/POST-chat.flag.test.js index 50f47b3572..acff8b507b 100644 --- a/test/api/v3/integration/chat/POST-chat.flag.test.js +++ b/test/api/v3/integration/chat/POST-chat.flag.test.js @@ -17,7 +17,7 @@ describe('POST /chat/:chatId/flag', () => { beforeEach(async () => { user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); - admin = await generateUser({ balance: 1, 'contributor.admin': true }); + admin = await generateUser({ balance: 1, 'permissions.moderator': true }); anotherUser = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); newUser = await generateUser({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() }); sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve()); diff --git a/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js b/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js index 9bbc122244..e9f77e5561 100644 --- a/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js +++ b/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js @@ -23,7 +23,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => { groupWithChat = group; author = groupLeader; nonAdmin = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); - admin = await generateUser({ 'contributor.admin': true }); + admin = await generateUser({ 'permissions.moderator': true }); message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' }); message = message.message; diff --git a/test/api/v3/integration/coupons/GET-coupons.test.js b/test/api/v3/integration/coupons/GET-coupons.test.js index 24cc230e49..0839f22dba 100644 --- a/test/api/v3/integration/coupons/GET-coupons.test.js +++ b/test/api/v3/integration/coupons/GET-coupons.test.js @@ -14,18 +14,18 @@ describe('GET /coupons/', () => { user = await generateUser(); }); - it('returns an error if user has no sudo permission', async () => { + it('returns an error if user has no coupons permission', async () => { await user.get('/user'); // needed so the request after this will authenticate with the correct cookie session await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: apiError('noSudoAccess'), + message: apiError('noPrivAccess'), }); }); it('should return the coupons in CSV format ordered by creation date', async () => { await user.update({ - 'contributor.sudo': true, + 'permissions.coupons': true, }); const coupons = await user.post('/coupons/generate/wondercon?count=11'); diff --git a/test/api/v3/integration/coupons/POST-coupons_enter_code.test.js b/test/api/v3/integration/coupons/POST-coupons_enter_code.test.js index d2b0fdec44..31830a0689 100644 --- a/test/api/v3/integration/coupons/POST-coupons_enter_code.test.js +++ b/test/api/v3/integration/coupons/POST-coupons_enter_code.test.js @@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => { beforeEach(async () => { user = await generateUser(); sudoUser = await generateUser({ - 'contributor.sudo': true, + 'permissions.coupons': true, }); }); diff --git a/test/api/v3/integration/coupons/POST-coupons_generate_event.test.js b/test/api/v3/integration/coupons/POST-coupons_generate_event.test.js index 591cf1a567..f0c5bfe436 100644 --- a/test/api/v3/integration/coupons/POST-coupons_generate_event.test.js +++ b/test/api/v3/integration/coupons/POST-coupons_generate_event.test.js @@ -14,19 +14,19 @@ describe('POST /coupons/generate/:event', () => { beforeEach(async () => { user = await generateUser({ - 'contributor.sudo': true, + 'permissions.coupons': true, }); }); - it('returns an error if user has no sudo permission', async () => { + it('returns an error if user has no coupons permission', async () => { await user.update({ - 'contributor.sudo': false, + 'permissions.coupons': false, }); await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: apiError('noSudoAccess'), + message: apiError('noPrivAccess'), }); }); @@ -48,7 +48,7 @@ describe('POST /coupons/generate/:event', () => { it('should generate coupons', async () => { await user.update({ - 'contributor.sudo': true, + 'permissions.coupons': true, }); const coupons = await user.post('/coupons/generate/wondercon?count=2'); diff --git a/test/api/v3/integration/coupons/POST-coupons_validate_code.test.js b/test/api/v3/integration/coupons/POST-coupons_validate_code.test.js index 5d6bef2b91..a206d245c7 100644 --- a/test/api/v3/integration/coupons/POST-coupons_validate_code.test.js +++ b/test/api/v3/integration/coupons/POST-coupons_validate_code.test.js @@ -21,7 +21,7 @@ describe('POST /coupons/validate/:code', () => { it('returns true if coupon code is valid', async () => { const sudoUser = await generateUser({ - 'contributor.sudo': true, + 'permissions.coupons': true, }); const [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1'); diff --git a/test/api/v3/integration/debug/POST-debug_make-admin.test.js b/test/api/v3/integration/debug/POST-debug_make-admin.test.js index 311d477994..c03fa4627f 100644 --- a/test/api/v3/integration/debug/POST-debug_make-admin.test.js +++ b/test/api/v3/integration/debug/POST-debug_make-admin.test.js @@ -3,7 +3,7 @@ import { generateUser, } from '../../../../helpers/api-integration/v3'; -describe('POST /debug/make-admin (pended for v3 prod testing)', () => { +describe('POST /debug/make-admin', () => { let user; before(async () => { @@ -14,12 +14,12 @@ describe('POST /debug/make-admin (pended for v3 prod testing)', () => { nconf.set('IS_PROD', false); }); - it('makes user an admine', async () => { + it('makes user an admin', async () => { await user.post('/debug/make-admin'); await user.sync(); - expect(user.contributor.admin).to.eql(true); + expect(user.permissions.fullAccess).to.eql(true); }); it('returns error when not in production mode', async () => { diff --git a/test/api/v3/integration/groups/GET-groups.test.js b/test/api/v3/integration/groups/GET-groups.test.js index aaea5b397d..74c988e0b1 100644 --- a/test/api/v3/integration/groups/GET-groups.test.js +++ b/test/api/v3/integration/groups/GET-groups.test.js @@ -219,11 +219,19 @@ describe('GET /groups', () => { it('returns 30 guilds per page ordered by number of members', async () => { await user.update({ balance: 9000 }); - const groups = await Promise.all(_.times(60, i => generateGroup(user, { - name: `public guild ${i} - is member`, - type: 'guild', - privacy: 'public', - }))); + const delay = () => new Promise(resolve => setTimeout(resolve, 40)); + const promises = []; + + for (let i = 0; i < 60; i += 1) { + promises.push(generateGroup(user, { + name: `public guild ${i} - is member`, + type: 'guild', + privacy: 'public', + })); + await delay(); // eslint-disable-line no-await-in-loop + } + + const groups = await Promise.all(promises); // update group number 32 and not the first to make sure sorting works await groups[32].update({ name: 'guild with most members', memberCount: 199 }); diff --git a/test/api/v3/integration/groups/GET-groups_id.test.js b/test/api/v3/integration/groups/GET-groups_id.test.js index 801ee3d434..7d1657db18 100644 --- a/test/api/v3/integration/groups/GET-groups_id.test.js +++ b/test/api/v3/integration/groups/GET-groups_id.test.js @@ -315,7 +315,7 @@ describe('GET /groups/:id', () => { beforeEach(async () => { admin = await generateUser({ - 'contributor.admin': true, + 'permissions.moderator': true, }); }); diff --git a/test/api/v3/integration/groups/POST-groups.test.js b/test/api/v3/integration/groups/POST-groups.test.js index 934281209f..b507fc8165 100644 --- a/test/api/v3/integration/groups/POST-groups.test.js +++ b/test/api/v3/integration/groups/POST-groups.test.js @@ -2,6 +2,7 @@ import { generateUser, translate as t, } from '../../../../helpers/api-integration/v3'; +import { model as Group } from '../../../../../website/server/models/group'; describe('POST /group', () => { let user; @@ -203,6 +204,23 @@ describe('POST /group', () => { expect(updatedUser.balance).to.eql(user.balance - 1); }); + + it('does not deduct the gems from user when guild creation fails', async () => { + const stub = sinon.stub(Group.prototype, 'save').rejects(); + const promise = user.post('/groups', { + name: groupName, + type: groupType, + privacy: groupPrivacy, + }); + + await expect(promise).to.eventually.be.rejected; + + const updatedUser = await user.get('/user'); + + expect(updatedUser.balance).to.eql(user.balance); + + stub.restore(); + }); }); }); diff --git a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js index d91267ed2b..7b7fc0e56e 100644 --- a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js +++ b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js @@ -32,7 +32,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => { invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring member = members[0]; // eslint-disable-line prefer-destructuring member2 = members[1]; // eslint-disable-line prefer-destructuring - adminUser = await generateUser({ 'contributor.admin': true }); + adminUser = await generateUser({ 'permissions.moderator': true }); }); context('All Groups', () => { diff --git a/test/api/v3/integration/groups/PUT-groups.test.js b/test/api/v3/integration/groups/PUT-groups.test.js index 61bb6370bc..1ff67958da 100644 --- a/test/api/v3/integration/groups/PUT-groups.test.js +++ b/test/api/v3/integration/groups/PUT-groups.test.js @@ -20,7 +20,7 @@ describe('PUT /group', () => { }, members: 1, }); - adminUser = await generateUser({ 'contributor.admin': true }); + adminUser = await generateUser({ 'permissions.moderator': true }); groupToUpdate = group; leader = groupLeader; nonLeader = members[0]; // eslint-disable-line prefer-destructuring @@ -104,11 +104,11 @@ describe('PUT /group', () => { // Update the bannedWordsAllowed property for the group const response = await groupLeader.put(`/groups/${group._id}`, updateGroupDetails); - expect(groupLeader.contributor.admin).to.eql(true); + expect(groupLeader.permissions.fullAccess).to.eql(true); expect(response.bannedWordsAllowed).to.eql(true); }); - it('does not allow for a non-admin to update the bannedWordsAllow property for an existing guild', async () => { + it('does not allow for a non-moderator to update the bannedWordsAllow property for an existing guild', async () => { const { group, groupLeader } = await createAndPopulateGroup({ groupDetails: { name: 'public guild', @@ -128,7 +128,6 @@ describe('PUT /group', () => { // Update the bannedWordsAllowed property for the group const response = await groupLeader.put(`/groups/${group._id}`, updateGroupDetails); - expect(groupLeader.contributor.admin).to.eql(undefined); expect(response.bannedWordsAllowed).to.eql(undefined); }); }); diff --git a/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js b/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js index bcafdcfd80..107f729a7d 100644 --- a/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js +++ b/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js @@ -7,9 +7,14 @@ import { describe('GET /heroes/:heroId', () => { let user; + const heroFields = [ + '_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items', + 'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', + ]; + before(async () => { user = await generateUser({ - contributor: { admin: true }, + permissions: { userSupport: true }, }); }); @@ -19,7 +24,7 @@ describe('GET /heroes/:heroId', () => { await expect(nonAdmin.get(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: t('noAdminAccess'), + message: t('noPrivAccess'), }); }); @@ -49,10 +54,7 @@ describe('GET /heroes/:heroId', () => { }); const heroRes = await user.get(`/hall/heroes/${hero._id}`); - expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys - '_id', 'id', 'balance', 'profile', 'purchased', - 'contributor', 'auth', 'items', 'secret', - ]); + expect(heroRes).to.have.all.keys(heroFields); // works as: object has all and only these keys expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); expect(heroRes.profile).to.have.all.keys(['name']); expect(heroRes.secret.text).to.be.eq('Super Hero'); @@ -64,10 +66,7 @@ describe('GET /heroes/:heroId', () => { }); const heroRes = await user.get(`/hall/heroes/${hero.auth.local.username}`); - expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys - '_id', 'id', 'balance', 'profile', 'purchased', - 'contributor', 'auth', 'items', 'secret', - ]); + expect(heroRes).to.have.all.keys(heroFields); expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); expect(heroRes.profile).to.have.all.keys(['name']); }); diff --git a/test/api/v3/integration/hall/GET-hall_heroes_party_groupId.test.js b/test/api/v3/integration/hall/GET-hall_heroes_party_groupId.test.js new file mode 100644 index 0000000000..ea61800d0c --- /dev/null +++ b/test/api/v3/integration/hall/GET-hall_heroes_party_groupId.test.js @@ -0,0 +1,61 @@ +import { v4 as generateUUID } from 'uuid'; +import { + generateUser, + generateGroup, + translate as t, +} from '../../../../helpers/api-integration/v3'; +import apiError from '../../../../../website/server/libs/apiError'; + +describe('GET /heroes/party/:groupId', () => { + let user; // admin user + + before(async () => { + user = await generateUser({ + 'permissions.userSupport': true, + }); + }); + + it('requires the caller to be an admin', async () => { + const nonAdmin = await generateUser(); + const party = await generateGroup(nonAdmin, { type: 'party', privacy: 'private' }); + await expect(nonAdmin.get(`/hall/heroes/party/${party._id}`)).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: apiError('noPrivAccess'), + }); + }); + + it('validates req.params.groupId', async () => { + await expect(user.get('/hall/heroes/party/invalidUUID')).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); + + it('handles non-existing party', async () => { + const dummyId = generateUUID(); + await expect(user.get(`/hall/heroes/party/${dummyId}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: apiError('groupWithIDNotFound', { groupId: dummyId }), + }); + }); + + it('returns only necessary party data given group id', async () => { + const nonAdmin = await generateUser(); + const party = await generateGroup(nonAdmin, { type: 'party', privacy: 'private' }); + + const partyRes = await user.get(`/hall/heroes/party/${party._id}`); + + expect(partyRes).to.have.all.keys([ // works as: object has all and only these keys + '_id', 'id', 'balance', 'challengeCount', 'leader', 'leaderOnly', 'memberCount', + 'purchased', 'quest', 'summary', + ]); + expect(partyRes.summary).to.eq(' '); + // NB: 'summary' is NOT a field that the API route retrieves! + // It must not be retrieved for privacy reasons. + // However the group model automatically adds a summary for reasons given here: + // https://github.com/HabitRPG/habitica/blob/8da36bf27c62ba0397a6af260c20d35a17f3d911/website/server/models/group.js#L161-L170 + }); +}); diff --git a/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js b/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js index 5e9d042ea5..2f98e89b3f 100644 --- a/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js +++ b/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js @@ -1,4 +1,5 @@ import { v4 as generateUUID } from 'uuid'; +import { model as User } from '../../../../../website/server/models/user'; import { generateUser, translate as t, @@ -8,15 +9,12 @@ describe('PUT /heroes/:heroId', () => { let user; const heroFields = [ - '_id', 'balance', 'profile', 'purchased', - 'contributor', 'auth', 'items', 'flags', - 'secret', + '_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron', + 'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', ]; before(async () => { - user = await generateUser({ - contributor: { admin: true }, - }); + user = await generateUser({ 'permissions.userSupport': true }); }); it('requires the caller to be an admin', async () => { @@ -25,7 +23,7 @@ describe('PUT /heroes/:heroId', () => { await expect(nonAdmin.put(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: t('noAdminAccess'), + message: t('noPrivAccess'), }); }); @@ -57,8 +55,7 @@ describe('PUT /heroes/:heroId', () => { }); // test response - // works as: object has all and only these keys - expect(heroRes).to.have.all.keys(heroFields); + expect(heroRes).to.have.all.keys(heroFields); // works as: object has all and only these keys expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); expect(heroRes.profile).to.have.all.keys(['name']); @@ -134,7 +131,6 @@ describe('PUT /heroes/:heroId', () => { }); // test response - // works as: object has all and only these keys expect(heroRes).to.have.all.keys(heroFields); expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); expect(heroRes.profile).to.have.all.keys(['name']); @@ -159,7 +155,6 @@ describe('PUT /heroes/:heroId', () => { }); // test response - // works as: object has all and only these keys expect(heroRes).to.have.all.keys(heroFields); expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); expect(heroRes.profile).to.have.all.keys(['name']); @@ -215,7 +210,6 @@ describe('PUT /heroes/:heroId', () => { }); // test response - // works as: object has all and only these keys expect(heroRes).to.have.all.keys(heroFields); expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); expect(heroRes.profile).to.have.all.keys(['name']); @@ -226,4 +220,35 @@ describe('PUT /heroes/:heroId', () => { await hero.sync(); expect(hero.items.special.snowball).to.equal(5); }); + + it('does not accidentally update API Token', async () => { + // This test has been included because hall.js will contain code to produce + // a truncated version of the API Token, and we want to be sure that + // the real Token is not modified by bugs in that code. + const hero = await generateUser(); + const originalToken = hero.apiToken; + + // make any change to the user except the Token + await user.put(`/hall/heroes/${hero._id}`, { + contributor: { text: 'Astronaut' }, + }); + + const updatedHero = await User.findById(hero._id).exec(); + expect(updatedHero.apiToken).to.equal(originalToken); + expect(updatedHero.apiTokenObscured).to.not.exist; + }); + + it('does update API Token when admin changes it', async () => { + const hero = await generateUser(); + const originalToken = hero.apiToken; + + // change the user's API Token + await user.put(`/hall/heroes/${hero._id}`, { + changeApiToken: true, + }); + + const updatedHero = await User.findById(hero._id).exec(); + expect(updatedHero.apiToken).to.not.equal(originalToken); + expect(updatedHero.apiTokenObscured).to.not.exist; + }); }); diff --git a/test/api/v3/integration/members/POST-send_private_message.test.js b/test/api/v3/integration/members/POST-send_private_message.test.js index a66c682286..a51ca111b8 100644 --- a/test/api/v3/integration/members/POST-send_private_message.test.js +++ b/test/api/v3/integration/members/POST-send_private_message.test.js @@ -176,7 +176,7 @@ describe('POST /members/send-private-message', () => { it('allows admin to send when sender has blocked the admin', async () => { userToSendMessage = await generateUser({ - 'contributor.admin': 1, + 'permissions.moderator': true, }); const receiver = await generateUser({ 'inbox.blocks': [userToSendMessage._id] }); @@ -204,7 +204,7 @@ describe('POST /members/send-private-message', () => { it('allows admin to send when to user has opted out of messaging', async () => { userToSendMessage = await generateUser({ - 'contributor.admin': 1, + 'permissions.moderator': true, }); const receiver = await generateUser({ 'inbox.optOut': true }); diff --git a/test/api/v3/integration/tasks/GET-tasks_id.test.js b/test/api/v3/integration/tasks/GET-tasks_id.test.js index a699815491..2e9353a9a3 100644 --- a/test/api/v3/integration/tasks/GET-tasks_id.test.js +++ b/test/api/v3/integration/tasks/GET-tasks_id.test.js @@ -105,7 +105,7 @@ describe('GET /tasks/:id', () => { it('can get challenge task if admin', async () => { const admin = await generateUser({ - 'contributor.admin': true, + 'permissions.challengeAdmin': true, }); const getTask = await admin.get(`/tasks/${task._id}`); diff --git a/test/api/v3/integration/tasks/challenges/POST-tasks_challenge_id.test.js b/test/api/v3/integration/tasks/challenges/POST-tasks_challenge_id.test.js index 272dd858fd..7d75753217 100644 --- a/test/api/v3/integration/tasks/challenges/POST-tasks_challenge_id.test.js +++ b/test/api/v3/integration/tasks/challenges/POST-tasks_challenge_id.test.js @@ -60,7 +60,7 @@ describe('POST /tasks/challenge/:challengeId', () => { }); it('allows non-leader admin to add tasks to a challenge when not a member', async () => { - const admin = await generateUser({ 'contributor.admin': true }); + const admin = await generateUser({ 'permissions.challengeAdmin': true }); const task = await admin.post(`/tasks/challenge/${challenge._id}`, { text: 'test habit from admin', type: 'habit', diff --git a/test/api/v3/integration/user/POST-user_reset.test.js b/test/api/v3/integration/user/POST-user_reset.test.js index 11f771ce5a..760c64df37 100644 --- a/test/api/v3/integration/user/POST-user_reset.test.js +++ b/test/api/v3/integration/user/POST-user_reset.test.js @@ -120,7 +120,7 @@ describe('POST /user/reset', () => { it('does not delete secret', async () => { const admin = await generateUser({ - contributor: { admin: true }, + permissions: { userSupport: true }, }); const hero = await generateUser({ diff --git a/test/api/v3/integration/user/PUT-user.test.js b/test/api/v3/integration/user/PUT-user.test.js index b2d6121741..1a0451318f 100644 --- a/test/api/v3/integration/user/PUT-user.test.js +++ b/test/api/v3/integration/user/PUT-user.test.js @@ -135,6 +135,7 @@ describe('PUT /user', () => { 'gem balance': { balance: 100 }, auth: { 'auth.blocked': true, 'auth.timestamps.created': new Date() }, contributor: { 'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text' }, + permissions: { 'permissions.fullAccess': true, 'permissions.news': true, 'permissions.moderator': '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 }, diff --git a/test/api/v4/coupon/POST-coupons_enter_code.test.js b/test/api/v4/coupon/POST-coupons_enter_code.test.js index 71e4c05719..acc6b51a66 100644 --- a/test/api/v4/coupon/POST-coupons_enter_code.test.js +++ b/test/api/v4/coupon/POST-coupons_enter_code.test.js @@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => { beforeEach(async () => { user = await generateUser(); sudoUser = await generateUser({ - 'contributor.sudo': true, + 'permissions.coupons': true, }); }); diff --git a/test/api/v4/members/GET-purchase_history.test.js b/test/api/v4/members/GET-purchase_history.test.js index 99623a4561..58d77f099a 100644 --- a/test/api/v4/members/GET-purchase_history.test.js +++ b/test/api/v4/members/GET-purchase_history.test.js @@ -8,7 +8,7 @@ describe('GET /members/:memberId/purchase-history', () => { before(async () => { user = await generateUser({ - contributor: { admin: true }, + permissions: { userSupport: true }, }); }); @@ -26,7 +26,7 @@ describe('GET /members/:memberId/purchase-history', () => { await expect(nonAdmin.get(`/members/${member._id}/purchase-history`)).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: t('noAdminAccess'), + message: t('noPrivAccess'), }); }); diff --git a/test/api/v4/news/DELETE-news.test.js b/test/api/v4/news/DELETE-news.test.js index 211c0d706d..5e1d540b2a 100644 --- a/test/api/v4/news/DELETE-news.test.js +++ b/test/api/v4/news/DELETE-news.test.js @@ -15,16 +15,16 @@ describe('DELETE /news/:newsID', () => { }; beforeEach(async () => { user = await generateUser({ - 'contributor.newsPoster': true, + 'permissions.news': true, }); }); it('disallows access to non-newsPosters', async () => { - const nonAdminUser = await generateUser({ 'contributor.newsPoster': false }); + const nonAdminUser = await generateUser({ 'permissions.news': false }); await expect(nonAdminUser.del(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: 'You don\'t have news poster access.', + message: t('noPrivAccess'), }); }); diff --git a/test/api/v4/news/GET-news.test.js b/test/api/v4/news/GET-news.test.js index 64db6d8391..9ea0eac2d1 100644 --- a/test/api/v4/news/GET-news.test.js +++ b/test/api/v4/news/GET-news.test.js @@ -15,7 +15,7 @@ describe('GET /news', () => { before(async () => { api = requester(); const user = await generateUser({ - 'contributor.newsPoster': true, + 'permissions.news': true, }); await Promise.all([ diff --git a/test/api/v4/news/GET-news_id.test.js b/test/api/v4/news/GET-news_id.test.js index a6b7ed0579..c0fdd46d04 100644 --- a/test/api/v4/news/GET-news_id.test.js +++ b/test/api/v4/news/GET-news_id.test.js @@ -15,7 +15,7 @@ describe('GET /news/:newsID', () => { }; beforeEach(async () => { user = await generateUser({ - 'contributor.newsPoster': true, + 'permissions.news': true, }); }); diff --git a/test/api/v4/news/POST-news.test.js b/test/api/v4/news/POST-news.test.js index f102616624..733204a632 100644 --- a/test/api/v4/news/POST-news.test.js +++ b/test/api/v4/news/POST-news.test.js @@ -16,16 +16,16 @@ describe('POST /news', () => { }; beforeEach(async () => { user = await generateUser({ - 'contributor.newsPoster': true, + 'permissions.news': true, }); }); it('disallows access to non-admins', async () => { - const nonAdminUser = await generateUser({ 'contributor.newsPoster': false }); + const nonAdminUser = await generateUser({ 'permissions.news': false }); await expect(nonAdminUser.post('/news')).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: 'You don\'t have news poster access.', + message: 'You don\'t have the required privileges.', }); }); diff --git a/test/api/v4/news/PUT-news_newsId.test.js b/test/api/v4/news/PUT-news_newsId.test.js index 6301b94aa3..172603a45a 100644 --- a/test/api/v4/news/PUT-news_newsId.test.js +++ b/test/api/v4/news/PUT-news_newsId.test.js @@ -17,16 +17,16 @@ describe('PUT /news/:newsID', () => { }; beforeEach(async () => { user = await generateUser({ - 'contributor.newsPoster': true, + 'permissions.news': true, }); }); it('disallows access to non-admins', async () => { - const nonAdminUser = await generateUser({ 'contributor.newsPoster': false }); + const nonAdminUser = await generateUser({ 'permissions.news': false }); await expect(nonAdminUser.put('/news/1234')).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: 'You don\'t have news poster access.', + message: 'You don\'t have the required privileges.', }); }); diff --git a/test/api/v4/user/POST-user_reset.test.js b/test/api/v4/user/POST-user_reset.test.js index 1c98059cc6..677bf193ce 100644 --- a/test/api/v4/user/POST-user_reset.test.js +++ b/test/api/v4/user/POST-user_reset.test.js @@ -120,7 +120,7 @@ describe('POST /user/reset', () => { it('does not delete secret', async () => { const admin = await generateUser({ - contributor: { admin: true }, + permissions: { userSupport: true }, }); const hero = await generateUser({ diff --git a/test/api/v4/user/PUT-user.test.js b/test/api/v4/user/PUT-user.test.js index 2fe1e32bc1..a588a6f386 100644 --- a/test/api/v4/user/PUT-user.test.js +++ b/test/api/v4/user/PUT-user.test.js @@ -84,6 +84,7 @@ describe('PUT /user', () => { 'gem balance': { balance: 100 }, auth: { 'auth.blocked': true, 'auth.timestamps.created': new Date() }, contributor: { 'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text' }, + permissions: { 'permissions.fullAccess': true, 'permissions.news': true, 'permissions.moderator': '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 }, diff --git a/website/client/src/components/admin-panel/filters/formatDate.js b/website/client/src/components/admin-panel/filters/formatDate.js new file mode 100644 index 0000000000..339fc08262 --- /dev/null +++ b/website/client/src/components/admin-panel/filters/formatDate.js @@ -0,0 +1,7 @@ +import moment from 'moment'; + +export default function formatDate (inputDate) { + if (!inputDate) return ''; + const date = moment(inputDate).utcOffset(0).format('YYYY-MM-DD HH:mm'); + return `${date} UTC`; +} diff --git a/website/client/src/components/admin-panel/index.vue b/website/client/src/components/admin-panel/index.vue new file mode 100644 index 0000000000..acd58c31d2 --- /dev/null +++ b/website/client/src/components/admin-panel/index.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/website/client/src/components/admin-panel/mixins/getItemDescription.js b/website/client/src/components/admin-panel/mixins/getItemDescription.js new file mode 100644 index 0000000000..0664f3a8fd --- /dev/null +++ b/website/client/src/components/admin-panel/mixins/getItemDescription.js @@ -0,0 +1,132 @@ +import content from '@/../../common/script/content'; + +function _getGearSetName (key) { + let set = 'NO SET [probably an omission in the API data]'; + if (content.gear.flat[key].set) { + set = `${content.gear.flat[key].set}`; + } + return set; +} + +function _getGearSetDescription (key) { + let setName = _getGearSetName(key); + if (setName === 'special-takeThis') { + // no point displaying set details for gear where it's obvious + return ''; + } + const klassNames = { + healer: 'Healer', + rogue: 'Rogue', + warrior: 'Warrior', + wizard: 'Mage', + }; + const lunarBattleQuestGear = ['armor_special_lunarWarriorArmor', 'head_special_lunarWarriorHelm', 'weapon_special_lunarScythe']; + + const loginIncentivesGear = ['armor_special_bardRobes', 'armor_special_dandySuit', 'armor_special_lunarWarriorArmor', 'armor_special_nomadsCuirass', 'armor_special_pageArmor', 'armor_special_samuraiArmor', 'armor_special_sneakthiefRobes', 'armor_special_snowSovereignRobes', 'back_special_snowdriftVeil', 'head_special_bardHat', 'head_special_clandestineCowl', 'head_special_dandyHat', 'head_special_kabuto', 'head_special_lunarWarriorHelm', 'head_special_pageHelm', 'head_special_snowSovereignCrown', 'head_special_spikedHelm', 'shield_special_diamondStave', 'shield_special_lootBag', 'shield_special_wakizashi', 'shield_special_wintryMirror', 'weapon_special_bardInstrument', 'weapon_special_fencingFoil', 'weapon_special_lunarScythe', 'weapon_special_nomadsScimitar', 'weapon_special_pageBanner', 'weapon_special_skeletonKey', 'weapon_special_tachi']; + + const goldQuestsGear = ['armor_special_finnedOceanicArmor', 'head_special_fireCoralCirclet', 'weapon_special_tridentOfCrashingTides', 'shield_special_moonpearlShield', 'head_special_pyromancersTurban', 'armor_special_pyromancersRobes', 'weapon_special_taskwoodsLantern', 'armor_special_mammothRiderArmor', 'head_special_mammothRiderHelm', 'weapon_special_mammothRiderSpear', 'shield_special_mammothRiderHorn', 'armor_special_roguishRainbowMessengerRobes', 'head_special_roguishRainbowMessengerHood', 'weapon_special_roguishRainbowMessage', 'shield_special_roguishRainbowMessage', 'eyewear_special_aetherMask', 'body_special_aetherAmulet', 'back_special_aetherCloak', 'weapon_special_aetherCrystals']; + + const animalGear = ['back_special_bearTail', 'back_special_cactusTail', 'back_special_foxTail', 'back_special_lionTail', 'back_special_pandaTail', 'back_special_pigTail', 'back_special_tigerTail', 'back_special_wolfTail', 'headAccessory_special_bearEars', 'headAccessory_special_cactusEars', 'headAccessory_special_foxEars', 'headAccessory_special_lionEars', 'headAccessory_special_pandaEars', 'headAccessory_special_pigEars', 'headAccessory_special_tigerEars', 'headAccessory_special_wolfEars']; + + let wantSetName = true; // some set names are useful, others aren't + let setType = '[cannot determine set type]'; + if (setName === 'base-0') { + setType = 'empty slot'; + wantSetName = false; + } else if (setName.includes('special-turkey')) { + setType = 'Turkey Day'; + wantSetName = false; + } else if (setName.includes('special-nye')) { + setType = 'New Year\'s Eve'; + wantSetName = false; + } else if (setName.includes('special-birthday')) { + setType = 'Habitica Birthday Bash'; + wantSetName = false; + } else if (setName.includes('special-0') || key === 'weapon_special_3') { + setType = 'Kickstarter 2013'; + wantSetName = false; + } else if (setName.includes('special-1')) { + setType = 'Contributor gear'; + wantSetName = false; + } else if (setName.includes('special-2') || key === 'shield_special_goldenknight') { + setType = 'Legendary Equipment'; + wantSetName = false; + } else if (setName.includes('special-wondercon')) { + setType = 'Unconventional Armor'; + wantSetName = false; + } else if (lunarBattleQuestGear.includes(key)) { + setType = 'Lunar Battle Quest Line'; + wantSetName = false; + } else if (loginIncentivesGear.includes(key)) { + setType = 'Check-In Incentive'; + wantSetName = false; + } else if (goldQuestsGear.includes(key)) { + setType = 'from Gold-Purchasable Quest Lines'; + wantSetName = false; + } else if (animalGear.includes(key)) { + setType = 'Animal Avatar Accessory Customisations'; + wantSetName = false; + } else if (!content.gear.flat[key].klass) { + setType = 'NO "klass" [omission in API data]'; + } else if (content.gear.flat[key].klass === 'armoire') { + setType = 'Armoire set'; + } else if (content.gear.flat[key].klass === 'mystery') { + setType = 'Mystery Items'; + setName = setName.replace(/mystery-(....)(..)/, '$1-$2'); + } else if (content.gear.flat[key].klass === 'special') { + const specialClass = content.gear.flat[key].specialClass || ''; + if (specialClass && Object.keys(klassNames).includes(specialClass)) { + setType = `Grand Gala ${klassNames[specialClass]} set`; + } else if (key.includes('special_gaymerx')) { + setType = 'GaymerX'; + wantSetName = false; + } else if (key.includes('special_ks2019')) { + setType = 'Kickstarter 2019'; + wantSetName = false; + } else { + setType = '[unknown set]'; + wantSetName = false; + } + } else if (Object.keys(klassNames).includes(content.gear.flat[key].klass)) { + // e.g., base class gear such as weapon_warrior_6 (Golden Sword) + setType = `base ${klassNames[content.gear.flat[key].klass]} gear`; + wantSetName = false; + } + return (wantSetName) ? `${setType}: ${setName}` : setType; +} + + +export default { + data () { + return { + content, + }; + }, + methods: { + getItemDescription (itemType, key) { + // Returns item name. Also returns other info for equipment. + + const simpleItemTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'special']; + if (simpleItemTypes.includes(itemType) && content[itemType][key]) { + return content[itemType][key].text(); + } + + if (itemType === 'mounts' && content.mountInfo[key]) { + return content.mountInfo[key].text(); + } + + if (itemType === 'pets' && content.petInfo[key]) { + return content.petInfo[key].text(); + } + + if (itemType === 'gear' && content.gear.flat[key]) { + const name = content.gear.flat[key].text(); + const description = _getGearSetDescription(key); + if (description) return `${name} -- ${description}`; + return name; + } + + return 'NO NAME - invalid item?'; + }, + }, +}; diff --git a/website/client/src/components/admin-panel/mixins/saveHero.js b/website/client/src/components/admin-panel/mixins/saveHero.js new file mode 100644 index 0000000000..8bbef7170b --- /dev/null +++ b/website/client/src/components/admin-panel/mixins/saveHero.js @@ -0,0 +1,20 @@ +export default { + methods: { + async saveHero ({ hero, msg = 'User', clearData }) { + await this.$store.dispatch('hall:updateHero', { heroDetails: hero }); + await this.$store.dispatch('snackbars:add', { + title: '', + text: `${msg} updated`, + type: 'info', + }); + + if (clearData) { + // Use clearData when the saved changes may affect data in other components + // (e.g., adding a contributor tier will increase the Gem balance) + // The admin should re-fetch the data if they need to keep working on that user. + this.$emit('clear-data'); + this.$router.push({ name: 'adminPanel' }); + } + }, + }, +}; diff --git a/website/client/src/components/admin-panel/user-support/avatarAndDrops.vue b/website/client/src/components/admin-panel/user-support/avatarAndDrops.vue new file mode 100644 index 0000000000..3790263f21 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/avatarAndDrops.vue @@ -0,0 +1,68 @@ + + + diff --git a/website/client/src/components/admin-panel/user-support/basicDetails.vue b/website/client/src/components/admin-panel/user-support/basicDetails.vue new file mode 100644 index 0000000000..9227b68a71 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/basicDetails.vue @@ -0,0 +1,34 @@ + + + diff --git a/website/client/src/components/admin-panel/user-support/contributorDetails.vue b/website/client/src/components/admin-panel/user-support/contributorDetails.vue new file mode 100644 index 0000000000..48b72b8358 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/contributorDetails.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/website/client/src/components/admin-panel/user-support/cronAndAuth.vue b/website/client/src/components/admin-panel/user-support/cronAndAuth.vue new file mode 100644 index 0000000000..1a28b044c7 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/cronAndAuth.vue @@ -0,0 +1,223 @@ + + + diff --git a/website/client/src/components/admin-panel/user-support/index.vue b/website/client/src/components/admin-panel/user-support/index.vue new file mode 100644 index 0000000000..d94d6d44c4 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/index.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/website/client/src/components/admin-panel/user-support/itemsOwned.vue b/website/client/src/components/admin-panel/user-support/itemsOwned.vue new file mode 100644 index 0000000000..83b25c3e29 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/itemsOwned.vue @@ -0,0 +1,289 @@ + + + + + diff --git a/website/client/src/components/admin-panel/user-support/partyAndQuest.vue b/website/client/src/components/admin-panel/user-support/partyAndQuest.vue new file mode 100644 index 0000000000..a00aa6bec8 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/partyAndQuest.vue @@ -0,0 +1,317 @@ + + + diff --git a/website/client/src/components/admin-panel/user-support/privilegesAndGems.vue b/website/client/src/components/admin-panel/user-support/privilegesAndGems.vue new file mode 100644 index 0000000000..9de6873469 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/privilegesAndGems.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/website/client/src/components/admin-panel/user-support/transactions.vue b/website/client/src/components/admin-panel/user-support/transactions.vue new file mode 100644 index 0000000000..b5809c7580 --- /dev/null +++ b/website/client/src/components/admin-panel/user-support/transactions.vue @@ -0,0 +1,52 @@ + + + diff --git a/website/client/src/components/appFooter.vue b/website/client/src/components/appFooter.vue index 8157e2abd8..8ca55b4063 100644 --- a/website/client/src/components/appFooter.vue +++ b/website/client/src/components/appFooter.vue @@ -589,7 +589,7 @@ export default { async makeAdmin () { await axios.post('/api/v4/debug/make-admin'); // @TODO: Notification.text('You are now an admin! - // Go to the Hall of Heroes to change your contributor level.'); + // Reload the website then go to Help > Admin Panel to set contributor level, etc.'); // @TODO: sync() }, openModifyInventoryModal () { diff --git a/website/client/src/components/challenges/challengeDetail.vue b/website/client/src/components/challenges/challengeDetail.vue index 5b6e4907d5..bacd28c6fc 100644 --- a/website/client/src/components/challenges/challengeDetail.vue +++ b/website/client/src/components/challenges/challengeDetail.vue @@ -321,7 +321,7 @@ import cloneDeep from 'lodash/cloneDeep'; import omit from 'lodash/omit'; import { v4 as uuid } from 'uuid'; -import { mapState } from '@/libs/store'; +import { userStateMixin } from '../../mixins/userState'; import memberSearchDropdown from '@/components/members/memberSearchDropdown'; import closeChallengeModal from './closeChallengeModal'; import Column from '../tasks/column'; @@ -358,7 +358,7 @@ export default { userLink, groupLink, }, - mixins: [challengeMemberSearchMixin], + mixins: [challengeMemberSearchMixin, userStateMixin], props: ['challengeId'], data () { return { @@ -387,7 +387,6 @@ export default { }; }, computed: { - ...mapState({ user: 'user.data' }), isMember () { return this.user.challenges.indexOf(this.challenge._id) !== -1; }, @@ -396,7 +395,7 @@ export default { return this.user._id === this.challenge.leader._id; }, isAdmin () { - return Boolean(this.user.contributor.admin); + return this.hasPermission(this.user, 'challengeAdmin'); }, canJoin () { return !this.isMember; diff --git a/website/client/src/components/challenges/challengeModal.vue b/website/client/src/components/challenges/challengeModal.vue index c17e2d7fce..35de6ff8b8 100644 --- a/website/client/src/components/challenges/challengeModal.vue +++ b/website/client/src/components/challenges/challengeModal.vue @@ -112,7 +112,7 @@
@@ -277,14 +277,15 @@ import clone from 'lodash/clone'; import throttle from 'lodash/throttle'; import markdownDirective from '@/directives/markdown'; +import { userStateMixin } from '../../mixins/userState'; import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '@/../../common/script/constants'; -import { mapState } from '@/libs/store'; export default { directives: { markdown: markdownDirective, }, + mixins: [userStateMixin], props: ['groupId'], data () { const categoryOptions = [ @@ -378,7 +379,6 @@ export default { }; }, computed: { - ...mapState({ user: 'user.data' }), creating () { return !this.workingChallenge.id; }, diff --git a/website/client/src/components/chat/chatCard.vue b/website/client/src/components/chat/chatCard.vue index ef0e5a1b06..a22914c727 100644 --- a/website/client/src/components/chat/chatCard.vue +++ b/website/client/src/components/chat/chatCard.vue @@ -5,7 +5,7 @@ class="mentioned-icon" >
{{ flagCountDescription }} @@ -54,7 +54,7 @@
@@ -68,7 +68,7 @@
@@ -202,7 +202,7 @@ import cloneDeep from 'lodash/cloneDeep'; import escapeRegExp from 'lodash/escapeRegExp'; import renderWithMentions from '@/libs/renderWithMentions'; -import { mapState } from '@/libs/store'; +import { userStateMixin } from '../../mixins/userState'; import userLink from '../userLink'; import deleteIcon from '@/assets/svg/delete.svg'; @@ -223,6 +223,7 @@ export default { return moment(value).toDate().toString(); }, }, + mixins: [userStateMixin], props: { msg: {}, groupId: {}, @@ -240,7 +241,6 @@ export default { }; }, computed: { - ...mapState({ user: 'user.data' }), isUserMentioned () { const message = this.msg; diff --git a/website/client/src/components/chat/chatMessages.vue b/website/client/src/components/chat/chatMessages.vue index c34bce5ce7..823d781789 100644 --- a/website/client/src/components/chat/chatMessages.vue +++ b/website/client/src/components/chat/chatMessages.vue @@ -149,7 +149,7 @@ import moment from 'moment'; import axios from 'axios'; import debounce from 'lodash/debounce'; import findIndex from 'lodash/findIndex'; -import { mapState } from '@/libs/store'; +import { userStateMixin } from '../../mixins/userState'; import Avatar from '../avatar'; import copyAsTodoModal from './copyAsTodoModal'; @@ -161,6 +161,7 @@ export default { chatCard, Avatar, }, + mixins: [userStateMixin], props: { chat: {}, groupType: {}, @@ -182,7 +183,6 @@ export default { }; }, computed: { - ...mapState({ user: 'user.data' }), // @TODO: We need a different lazy load mechnism. // But honestly, adding a paging route to chat would solve this messages () { @@ -214,7 +214,7 @@ export default { canViewFlag (message) { if (message.uuid === this.user._id) return true; if (!message.flagCount || message.flagCount < 2) return true; - return this.user.contributor.admin; + return this.hasPermission(this.user, 'moderator'); }, loadProfileCache: debounce(function loadProfileCache (screenPosition) { this._loadProfileCache(screenPosition); diff --git a/website/client/src/components/chat/reportFlagModal.vue b/website/client/src/components/chat/reportFlagModal.vue index ff12fc4589..53b2f0f020 100644 --- a/website/client/src/components/chat/reportFlagModal.vue +++ b/website/client/src/components/chat/reportFlagModal.vue @@ -23,7 +23,7 @@