mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Merge branch 'develop' into sabrecat/teams-rebase
This commit is contained in:
@@ -401,14 +401,14 @@ api.clearChatFlags = {
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
if (!user.contributor.admin) {
|
||||
if (!user.hasPermission('moderator')) {
|
||||
throw new NotAuthorized(res.t('messageGroupChatAdminClearFlagCount'));
|
||||
}
|
||||
|
||||
const group = await Group.getGroup({
|
||||
user,
|
||||
groupId,
|
||||
optionalMembership: user.contributor.admin,
|
||||
optionalMembership: user.hasPermission('moderator'),
|
||||
});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
@@ -550,7 +550,7 @@ api.deleteChat = {
|
||||
const message = await Chat.findOne({ _id: chatId }).exec();
|
||||
if (!message) throw new NotFound(res.t('messageGroupChatNotFound'));
|
||||
|
||||
if (user._id !== message.uuid && !user.contributor.admin) {
|
||||
if (user._id !== message.uuid && !user.hasPermission('moderator')) {
|
||||
throw new NotAuthorized(res.t('onlyCreatorOrAdminCanDeleteChat'));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
authWithHeaders,
|
||||
authWithSession,
|
||||
} from '../../middlewares/auth';
|
||||
import { ensureSudo } from '../../middlewares/ensureAccessRight';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import * as couponsLib from '../../libs/coupons';
|
||||
import apiError from '../../libs/apiError';
|
||||
import { model as Coupon } from '../../models/coupon';
|
||||
@@ -35,7 +35,7 @@ const api = {};
|
||||
api.getCoupons = {
|
||||
method: 'GET',
|
||||
url: '/coupons',
|
||||
middlewares: [authWithSession, ensureSudo],
|
||||
middlewares: [authWithSession, ensurePermission('coupons')],
|
||||
async handler (req, res) {
|
||||
const coupons = await Coupon.find().sort('createdAt').lean().exec();
|
||||
|
||||
@@ -70,7 +70,7 @@ api.getCoupons = {
|
||||
api.generateCoupons = {
|
||||
method: 'POST',
|
||||
url: '/coupons/generate/:event',
|
||||
middlewares: [authWithHeaders(), ensureSudo],
|
||||
middlewares: [authWithHeaders(), ensurePermission('coupons')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('event', apiError('eventRequired')).notEmpty();
|
||||
req.checkQuery('count', apiError('countRequired')).notEmpty().isNumeric();
|
||||
|
||||
@@ -90,7 +90,7 @@ api.setCron = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/debug/make-admin Sets contributor.admin to true
|
||||
* @api {post} /api/v3/debug/make-admin Sets admin privileges for current user
|
||||
* @apiName setCron
|
||||
* @apiGroup Development
|
||||
* @apiPermission Developers
|
||||
@@ -104,7 +104,7 @@ api.makeAdmin = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
user.contributor.admin = true;
|
||||
user.permissions.fullAccess = true;
|
||||
|
||||
await user.save();
|
||||
|
||||
|
||||
@@ -137,8 +137,12 @@ api.createGroup = {
|
||||
user.party._id = group._id;
|
||||
}
|
||||
|
||||
const results = await Promise.all([user.save(), group.save()]);
|
||||
const savedGroup = results[1];
|
||||
let savedGroup;
|
||||
|
||||
await Group.db.transaction(async session => {
|
||||
await user.save({ session });
|
||||
savedGroup = await group.save({ session });
|
||||
});
|
||||
|
||||
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]);
|
||||
@@ -463,12 +467,12 @@ api.updateGroup = {
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const optionalMembership = Boolean(user.contributor.admin);
|
||||
const optionalMembership = Boolean(user.hasPermission('moderator'));
|
||||
const group = await Group.getGroup({ user, groupId: req.params.groupId, optionalMembership });
|
||||
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (user.contributor.admin) {
|
||||
if (user.hasPermission('moderator')) {
|
||||
if (req.body.bannedWordsAllowed === true) {
|
||||
group.bannedWordsAllowed = true;
|
||||
} else {
|
||||
@@ -477,7 +481,7 @@ api.updateGroup = {
|
||||
}
|
||||
|
||||
if (group.leader !== user._id && group.type === 'party') throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
|
||||
else if (group.leader !== user._id && !user.contributor.admin) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
|
||||
else if (group.leader !== user._id && !user.hasPermission('moderator')) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
|
||||
|
||||
if (req.body.leader !== user._id && group.hasNotCancelled()) throw new NotAuthorized(res.t('cannotChangeLeaderWithActiveGroupPlan'));
|
||||
|
||||
@@ -932,7 +936,7 @@ api.removeGroupMember = {
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const optionalMembership = Boolean(user.contributor.admin);
|
||||
const optionalMembership = Boolean(user.hasPermission('moderator'));
|
||||
const group = await Group.getGroup({
|
||||
user, groupId: req.params.groupId, optionalMembership, fields: '-chat',
|
||||
}); // Do not fetch chat
|
||||
@@ -942,9 +946,9 @@ api.removeGroupMember = {
|
||||
const uuid = req.params.memberId;
|
||||
|
||||
if (group.leader !== user._id && group.type === 'party') throw new NotAuthorized(res.t('onlyLeaderCanRemoveMember'));
|
||||
if (group.leader !== user._id && !user.contributor.admin) throw new NotAuthorized(res.t('onlyLeaderCanRemoveMember'));
|
||||
if (group.leader !== user._id && !user.hasPermission('moderator')) throw new NotAuthorized(res.t('onlyLeaderCanRemoveMember'));
|
||||
|
||||
if (group.leader === uuid && user.contributor.admin) throw new NotAuthorized(res.t('cannotRemoveCurrentLeader'));
|
||||
if (group.leader === uuid && user.hasPermission('moderator')) throw new NotAuthorized(res.t('cannotRemoveCurrentLeader'));
|
||||
|
||||
if (user._id === uuid) throw new NotAuthorized(res.t('memberCannotRemoveYourself'));
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import validator from 'validator';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { ensureAdmin } from '../../middlewares/ensureAccessRight';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as Group } from '../../models/group';
|
||||
import common from '../../../common';
|
||||
import {
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
@@ -144,7 +146,12 @@ api.getHeroes = {
|
||||
// Note, while the following routes are called getHero / updateHero
|
||||
// they can be used by admins to get/update any user
|
||||
|
||||
const heroAdminFields = 'contributor balance profile.name purchased items auth flags.chatRevoked flags.chatShadowMuted secret';
|
||||
const heroAdminFields = 'auth balance contributor flags items lastCron party preferences profile.name purchased secret permissions';
|
||||
const heroAdminFieldsToFetch = heroAdminFields; // these variables will make more sense when...
|
||||
const heroAdminFieldsToShow = heroAdminFields; // ... apiTokenObscured is added
|
||||
|
||||
const heroPartyAdminFields = 'balance challengeCount leader leaderOnly memberCount purchased quest';
|
||||
// must never include Party name, description, summary, leaderMessage
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/hall/heroes/:heroId Get any user ("hero") given the UUID or Username
|
||||
@@ -153,7 +160,7 @@ const heroAdminFields = 'contributor balance profile.name purchased items auth f
|
||||
* @apiGroup Hall
|
||||
* @apiPermission Admin
|
||||
*
|
||||
* @apiDescription Returns the profile of the given user. User does not need to be a contributor.
|
||||
* @apiDescription Returns various data about the user. User does not need to be a contributor.
|
||||
*
|
||||
* @apiSuccess {Object} data The user object
|
||||
*
|
||||
@@ -165,7 +172,7 @@ const heroAdminFields = 'contributor balance profile.name purchased items auth f
|
||||
api.getHero = {
|
||||
method: 'GET',
|
||||
url: '/hall/heroes/:heroId',
|
||||
middlewares: [authWithHeaders(), ensureAdmin],
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('heroId', res.t('heroIdRequired')).notEmpty();
|
||||
|
||||
@@ -183,16 +190,17 @@ api.getHero = {
|
||||
|
||||
const hero = await User
|
||||
.findOne(query)
|
||||
.select(heroAdminFields)
|
||||
.select(heroAdminFieldsToFetch)
|
||||
.exec();
|
||||
|
||||
if (!hero) throw new NotFound(res.t('userWithIDNotFound', { userId: heroId }));
|
||||
const heroRes = hero.toJSON({ minimize: true });
|
||||
heroRes.secret = hero.getSecretData();
|
||||
|
||||
// supply to the possible absence of hero.contributor
|
||||
// if we didn't pass minimize: true it would have returned all fields as empty
|
||||
if (!heroRes.contributor) heroRes.contributor = {};
|
||||
|
||||
heroRes.secret = hero.getSecretData();
|
||||
|
||||
res.respond(200, heroRes);
|
||||
},
|
||||
};
|
||||
@@ -209,9 +217,8 @@ const gemsPerTier = {
|
||||
* @apiGroup Hall
|
||||
* @apiPermission Admin
|
||||
*
|
||||
* @apiDescription Update user's gem balance, contributions and contribution tier,
|
||||
* or admin status. Grant items. Block / unblock user's account.
|
||||
* Revoke / unrevoke chat privileges.
|
||||
* @apiDescription Update various details in the user's User document,
|
||||
* including but not limited to privileges, gems, contributions, items.
|
||||
*
|
||||
* @apiExample Example Body:
|
||||
* {
|
||||
@@ -224,12 +231,17 @@ const gemsPerTier = {
|
||||
* "purchased": {"ads": true},
|
||||
* "contributor": {
|
||||
* "admin": true,
|
||||
* "newsPoster": false,
|
||||
* "contributions": "Improving API documentation",
|
||||
* "level": 5,
|
||||
* "text": "Scribe, Blacksmith"
|
||||
* },
|
||||
* "secret": {
|
||||
* "text": "child with permission to use site",
|
||||
* },
|
||||
* "itemPath": "items.pets.BearCub-Skeleton",
|
||||
* "itemVal": 1
|
||||
* "itemVal": 5,
|
||||
* "changeApiToken": true,
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} data The updated user object
|
||||
@@ -242,7 +254,7 @@ const gemsPerTier = {
|
||||
api.updateHero = {
|
||||
method: 'PUT',
|
||||
url: '/hall/heroes/:heroId',
|
||||
middlewares: [authWithHeaders(), ensureAdmin],
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
const { heroId } = req.params;
|
||||
const updateData = req.body;
|
||||
@@ -275,6 +287,7 @@ api.updateHero = {
|
||||
}
|
||||
|
||||
if (updateData.contributor) _.assign(hero.contributor, updateData.contributor);
|
||||
if (updateData.permissions && res.locals.user.hasPermission('userSupport')) _.assign(hero.permissions, updateData.permissions);
|
||||
if (updateData.purchased && updateData.purchased.ads) {
|
||||
hero.purchased.ads = updateData.purchased.ads;
|
||||
}
|
||||
@@ -310,11 +323,13 @@ api.updateHero = {
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.changeApiToken) hero.apiToken = common.uuid();
|
||||
|
||||
const savedHero = await hero.save();
|
||||
const heroJSON = savedHero.toJSON();
|
||||
heroJSON.secret = savedHero.getSecretData();
|
||||
const responseHero = { _id: heroJSON._id }; // only respond with important fields
|
||||
heroAdminFields.split(' ').forEach(field => {
|
||||
heroAdminFieldsToShow.split(' ').forEach(field => {
|
||||
_.set(responseHero, field, _.get(heroJSON, field));
|
||||
});
|
||||
|
||||
@@ -322,4 +337,49 @@ api.updateHero = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/hall/heroes/party/:groupId Get any Party given its ID
|
||||
* @apiParam (Path) {UUID} groupId party's group ID
|
||||
* @apiName GetHeroParty
|
||||
* @apiGroup Hall
|
||||
* @apiPermission userSupport
|
||||
*
|
||||
* @apiDescription Returns some basic information about a given Party,
|
||||
* to assist admins with user support.
|
||||
*
|
||||
* @apiSuccess {Object} data The party object (contains computed fields
|
||||
* that are not in the Group model)
|
||||
*
|
||||
* @apiUse NoAuthHeaders
|
||||
* @apiUse NoAccount
|
||||
* @apiUse NoUser
|
||||
* @apiUse NoPrivs
|
||||
* @apiUse groupIdRequired
|
||||
* @apiUse GroupNotFound
|
||||
*/
|
||||
api.getHeroParty = { // @TODO XXX add tests
|
||||
method: 'GET',
|
||||
url: '/hall/heroes/party/:groupId',
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { groupId } = req.params;
|
||||
|
||||
const query = { _id: groupId, type: 'party' };
|
||||
|
||||
const party = await Group
|
||||
.findOne(query)
|
||||
.select(heroPartyAdminFields)
|
||||
.exec();
|
||||
|
||||
if (!party) throw new NotFound(apiError('groupWithIDNotFound', { groupId }));
|
||||
const partyRes = party.toJSON();
|
||||
res.respond(200, partyRes);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -668,7 +668,7 @@ api.sendPrivateMessage = {
|
||||
if (!receiver.flags.verifiedUsername) delete receiver.auth.local.username;
|
||||
|
||||
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
|
||||
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
|
||||
if (objections.length > 0 && !sender.hasPermission('moderator')) throw new NotAuthorized(res.t(objections[0]));
|
||||
|
||||
const messageSent = await sentMessage(sender, receiver, message, res.t);
|
||||
|
||||
|
||||
@@ -382,6 +382,7 @@ api.getUserAnonymized = {
|
||||
delete user.achievements.challenges;
|
||||
delete user.notifications;
|
||||
delete user.secret;
|
||||
delete user.permissions;
|
||||
|
||||
_.forEach(user.inbox.messages, msg => {
|
||||
msg.text = 'inbox message text';
|
||||
|
||||
Reference in New Issue
Block a user