Merge branch 'develop' into party-chat-translations
# Conflicts: # test/api/unit/models/group.test.js
This commit is contained in:
@@ -327,7 +327,13 @@ api.loginSocial = {
|
||||
} else { // Create new user
|
||||
user = {
|
||||
auth: {
|
||||
[network]: profile,
|
||||
[network]: {
|
||||
id: profile.id,
|
||||
emails: profile.emails,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
name: profile.displayName || profile.name || profile.username,
|
||||
},
|
||||
preferences: {
|
||||
language: req.language,
|
||||
|
||||
@@ -312,7 +312,7 @@ api.leaveChallenge = {
|
||||
if (!challenge.isMember(user)) throw new NotAuthorized(res.t('challengeMemberNotFound'));
|
||||
|
||||
// Unlink challenge's tasks from user's tasks and save the challenge
|
||||
await Promise.all([challenge.unlinkTasks(user, keep), challenge.save()]);
|
||||
await challenge.unlinkTasks(user, keep);
|
||||
|
||||
res.analytics.track('challenge leave', {
|
||||
uuid: user._id,
|
||||
|
||||
@@ -196,6 +196,7 @@ api.createGroupPlan = {
|
||||
|
||||
// @TODO: Change message
|
||||
if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate'));
|
||||
group.memberCount = await User.count({ $or: [{ 'party._id': group._id }, { guilds: group._id }] }).exec();
|
||||
group.leader = user._id;
|
||||
user.guilds.push(group._id);
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ api.getMember = {
|
||||
|
||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||
let memberToJSON = member.toJSON({minimize: true});
|
||||
member.addComputedStatsToJSONObj(memberToJSON.stats);
|
||||
User.addComputedStatsToJSONObj(memberToJSON.stats, member);
|
||||
|
||||
res.respond(200, memberToJSON);
|
||||
},
|
||||
@@ -282,16 +282,12 @@ function _getMembersForItem (type) {
|
||||
.sort({_id: 1})
|
||||
.limit(limit)
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||
let membersToJSON = members.map(member => {
|
||||
let memberToJSON = member.toJSON({minimize: true});
|
||||
if (addComputedStats) member.addComputedStatsToJSONObj(memberToJSON.stats);
|
||||
|
||||
return memberToJSON;
|
||||
});
|
||||
res.respond(200, membersToJSON);
|
||||
members.forEach(member => User.transformJSONUser(member, addComputedStats));
|
||||
res.respond(200, members);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
|
||||
let api = {};
|
||||
|
||||
// @TODO export this const, cannot export it from here because only routes are exported from controllers
|
||||
const LAST_ANNOUNCEMENT_TITLE = 'FAIRY HATCHING POTIONS RETURN!';
|
||||
const LAST_ANNOUNCEMENT_TITLE = 'SUMMER SPLASH BEGINS: SUMMER CLASS OUTFITS, SEASONAL SHOP, AND NPC DECORATIONS!';
|
||||
const worldDmg = { // @TODO
|
||||
bailey: false,
|
||||
};
|
||||
@@ -32,12 +32,33 @@ api.getNews = {
|
||||
<h1 class="align-self-center">${res.t('newStuff')}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<h2>5/15/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
||||
<h2>6/19/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
||||
<hr/>
|
||||
<p>They're back! Between now and May 31, you can buy Fairy Hatching Potions from <a href='/shops/market' target='_blank'>the Market</a> and use them to hatch any standard pet egg. (Magic Hatching Potions do not work on Quest Pet eggs.) Fairy Potion Pets aren't picky, so they'll happily eat any kind of food that you feed them!</p>
|
||||
<p>After they're gone, it will be at least a year before the Fairy Hatching Potions are available again, so be sure to get them now!</p>
|
||||
<div class="small mb-3">by Edge, Willow the Witty, and SabreCat</div>
|
||||
<div class="promo_fairy_potions center-block"></div>
|
||||
<p>To escape the summer heat in Habit City, everyone's moved down to the undersea city of Dilatory. The Summer Splash event has begun!</p>
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">
|
||||
<h3>Summer Class Outfits</h3>
|
||||
<p>From now until July 31st, limited edition outfits are available in the Rewards column. Depending on your class, you can be a Lionfish Mage, Fisher-Rogue, Betta Fish Warrior, or Merfolk Monarch Healer! You'd better get productive to earn enough gold before they disappear. Good luck!</p>
|
||||
<div class="small mb-3">by Vampitch, Vikte, TheDudeAbides, Lalaitha, and Beffymaroo</div>
|
||||
<div class="media align-items-center">
|
||||
<div class="promo_seasonal_shop_summer mr-3"></div>
|
||||
<div class="media-body">
|
||||
<h3>Seasonal Shop is Open!</h3>
|
||||
<p>The <a href='/shops/seasonal' target='_blank'>Seasonal Shop</a> has opened! The Seasonal Sorceress is stocking the seasonal edition versions of previous summer outfits, now available for Gems instead of Gold. Plus, there will be more fun things in the shop as the event progresses. The Seasonal Shop will only be open until July 31st, so don't wait!</p>
|
||||
<div class="small mb-3">by SabreCat, Lemoness, AnnDeLune, Vampitch, nonight, tricksy.fox, Giu09, JaizakAripaik, Teto Forever, and Kai</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">
|
||||
<h3>NPC Costumes and Shop Decorations</h3>
|
||||
<p>Looks like the NPCs are really getting in to the cheery summer mood around the site. Who wouldn't? After all, there's plenty more celebration to come...</p>
|
||||
<div class="small mb-3">by Lemoness and Beffymaroo</div>
|
||||
</div>
|
||||
<div class="npc_matt ml-3 mb-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="promo_summer_splash_2018 ml-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ api.inviteToQuest = {
|
||||
'party._id': group._id,
|
||||
_id: {$ne: user._id},
|
||||
})
|
||||
.select('auth.facebook auth.local preferences.emailNotifications profile.name pushDevices')
|
||||
.select('auth.facebook auth.google auth.local preferences.emailNotifications profile.name pushDevices')
|
||||
.exec();
|
||||
|
||||
group.markModified('quest');
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
basicFields as basicGroupFields,
|
||||
model as Group,
|
||||
} from '../../models/group';
|
||||
import {
|
||||
model as User,
|
||||
} from '../../models/user';
|
||||
import * as Tasks from '../../models/task';
|
||||
import _ from 'lodash';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
@@ -90,7 +93,7 @@ api.getUser = {
|
||||
let {daysMissed} = user.daysUserHasMissed(new Date(), req);
|
||||
userToJSON.needsCron = false;
|
||||
if (daysMissed > 0) userToJSON.needsCron = true;
|
||||
user.addComputedStatsToJSONObj(userToJSON.stats);
|
||||
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
|
||||
}
|
||||
|
||||
return res.respond(200, userToJSON);
|
||||
@@ -830,7 +833,9 @@ api.buyMysterySet = {
|
||||
*
|
||||
* @apiErrorExample {json} Quest chosen does not exist
|
||||
* {"success":false,"error":"NotFound","message":"Quest \"dilatoryDistress99\" not found."}
|
||||
* @apiErrorExample {json} NotAuthorized Not enough gold
|
||||
* @apiErrorExample {json} You must first complete this quest's prerequisites
|
||||
* {"success":false,"error":"NotAuthorized","message":"You must first complete dilatoryDistress2."}
|
||||
* @apiErrorExample {json} NotAuthorized Not enough gold
|
||||
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
||||
*
|
||||
*/
|
||||
@@ -912,7 +917,7 @@ api.buySpecialSpell = {
|
||||
* }
|
||||
*
|
||||
* @apiError {NotAuthorized} messageAlreadyPet Already have the specific pet combination
|
||||
* @apiError {NotFound} messageMissingEggPotion One or both of the ingrediants are missing.
|
||||
* @apiError {NotFound} messageMissingEggPotion One or both of the ingredients are missing.
|
||||
* @apiError {NotFound} messageInvalidEggPotionCombo Cannot use that combination of egg and potion.
|
||||
*
|
||||
* @apiErrorExample {json} Already have that pet.
|
||||
@@ -1902,7 +1907,7 @@ api.movePinnedItem = {
|
||||
let currentPinnedItemPath = user.pinnedItemsOrder[currentIndex];
|
||||
|
||||
if (currentIndex === -1) {
|
||||
throw new BadRequest(res.t('wrongItemPath', req.language));
|
||||
throw new BadRequest(res.t('wrongItemPath', {path}, req.language));
|
||||
}
|
||||
|
||||
// Remove the one we will move
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
See https://github.com/HabitRPG/habitica/pull/10453
|
||||
@@ -0,0 +1,29 @@
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { toArray, orderBy } from 'lodash';
|
||||
|
||||
let api = {};
|
||||
|
||||
/* NOTE most inbox routes are either in the user or members controller */
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/inbox/messages Get inbox messages for a user
|
||||
* @apiPrivate
|
||||
* @apiName GetInboxMessages
|
||||
* @apiGroup Inbox
|
||||
* @apiDescription Get inbox messages for a user
|
||||
*
|
||||
* @apiSuccess {Array} data An array of inbox messages
|
||||
*/
|
||||
api.getInboxMessages = {
|
||||
method: 'GET',
|
||||
url: '/inbox/messages',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const messagesObj = res.locals.user.inbox.messages;
|
||||
const messagesArray = orderBy(toArray(messagesObj), ['timestamp'], ['desc']);
|
||||
|
||||
res.respond(200, messagesArray);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
@@ -82,12 +82,10 @@ export async function createChallenge (user, req, res) {
|
||||
addUserJoinChallengeNotification(user);
|
||||
|
||||
let results = await Promise.all([challenge.save({
|
||||
validateBeforeSave: false, // already validate
|
||||
}), group.save()]);
|
||||
validateBeforeSave: false, // already validated
|
||||
}), group.save(), user.save()]);
|
||||
let savedChal = results[0];
|
||||
|
||||
await savedChal.syncToUser(user); // (it also saves the user)
|
||||
|
||||
return {savedChal, group};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,14 +35,6 @@ export function getUserInfo (user, fields = []) {
|
||||
|
||||
if (fields.indexOf('name') !== -1) {
|
||||
info.name = user.profile && user.profile.name;
|
||||
|
||||
if (!info.name) {
|
||||
if (user.auth.local && user.auth.local.username) {
|
||||
info.name = user.auth.local.username;
|
||||
} else if (user.auth.facebook) {
|
||||
info.name = user.auth.facebook.displayName || user.auth.facebook.username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.indexOf('email') !== -1) {
|
||||
|
||||
@@ -35,7 +35,7 @@ export function sha1MakeSalt (len = 10) {
|
||||
}
|
||||
|
||||
// Compare the password for an user
|
||||
// Works with bcrypt and sha1 indipendently
|
||||
// Works with bcrypt and sha1 independently
|
||||
// An async function is used so that a promise is always returned
|
||||
// even for comparing sha1 hashed passwords that use a sync method
|
||||
export async function compare (user, passwordToCheck) {
|
||||
|
||||
@@ -28,7 +28,7 @@ async function buyGemGift (data) {
|
||||
|
||||
const senderMsg = getGiftMessage(data, byUsername, gemAmount, languages[0]);
|
||||
const receiverMsg = getGiftMessage(data, byUsername, gemAmount, languages[1]);
|
||||
data.user.sendMessage(data.gift.member, { receiverMsg, senderMsg });
|
||||
data.user.sendMessage(data.gift.member, { receiverMsg, senderMsg, save: false });
|
||||
|
||||
if (data.gift.member.preferences.emailNotifications.giftedGems !== false) {
|
||||
txnEmail(data.gift.member, 'gifted-gems', [
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import stripeModule from 'stripe';
|
||||
import nconf from 'nconf';
|
||||
import cc from 'coupon-code';
|
||||
import moment from 'moment';
|
||||
|
||||
import logger from '../logger';
|
||||
import {
|
||||
BadRequest,
|
||||
@@ -10,38 +8,22 @@ import {
|
||||
} from '../errors';
|
||||
import payments from './payments';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as Coupon } from '../../models/coupon';
|
||||
import {
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../models/group';
|
||||
import shared from '../../../common';
|
||||
import stripeConstants from './stripe/constants';
|
||||
import { checkout } from './stripe/checkout';
|
||||
import { getStripeApi, setStripeApi } from './stripe/api';
|
||||
|
||||
let stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
||||
const i18n = shared.i18n;
|
||||
|
||||
let api = {};
|
||||
|
||||
api.constants = {
|
||||
// CURRENCY_CODE: 'USD',
|
||||
// SELLER_NOTE: 'Habitica Payment',
|
||||
// SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
|
||||
// SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
|
||||
// STORE_NAME: 'Habitica',
|
||||
//
|
||||
// GIFT_TYPE_GEMS: 'gems',
|
||||
// GIFT_TYPE_SUBSCRIPTION: 'subscription',
|
||||
//
|
||||
// METHOD_BUY_GEMS: 'buyGems',
|
||||
// METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
|
||||
PAYMENT_METHOD: 'Stripe',
|
||||
// PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
|
||||
};
|
||||
|
||||
api.setStripeApi = function setStripeApi (stripeInc) {
|
||||
stripe = stripeInc;
|
||||
};
|
||||
api.constants = Object.assign({}, stripeConstants);
|
||||
|
||||
api.setStripeApi = setStripeApi;
|
||||
|
||||
/**
|
||||
* Allows for purchasing a user subscription, group subscription or gems with Stripe
|
||||
@@ -56,110 +38,7 @@ api.setStripeApi = function setStripeApi (stripeInc) {
|
||||
* @param options.headers The request headers to store on analytics
|
||||
* @return undefined
|
||||
*/
|
||||
api.checkout = async function checkout (options, stripeInc) {
|
||||
let {
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
} = options;
|
||||
let response;
|
||||
let subscriptionId;
|
||||
|
||||
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
|
||||
let stripeApi = stripe;
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
if (!token) throw new BadRequest('Missing req.body.id');
|
||||
|
||||
if (gift) {
|
||||
const member = await User.findById(gift.uuid).exec();
|
||||
gift.member = member;
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
if (sub.discount) {
|
||||
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
|
||||
coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
|
||||
if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon'));
|
||||
}
|
||||
|
||||
let customerObject = {
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
};
|
||||
|
||||
if (groupId) {
|
||||
customerObject.quantity = sub.quantity;
|
||||
const groupFields = basicGroupFields.concat(' purchased');
|
||||
const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||
const membersCount = await group.getMemberCount();
|
||||
customerObject.quantity = membersCount + sub.quantity - 1;
|
||||
}
|
||||
|
||||
response = await stripeApi.customers.create(customerObject);
|
||||
|
||||
if (groupId) subscriptionId = response.subscriptions.data[0].id;
|
||||
} else {
|
||||
let amount = 500; // $5
|
||||
|
||||
if (gift) {
|
||||
if (gift.type === 'subscription') {
|
||||
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
|
||||
} else {
|
||||
if (gift.gems.amount <= 0) {
|
||||
throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase'));
|
||||
}
|
||||
amount = `${gift.gems.amount / 4 * 100}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gift || gift.type === 'gems') {
|
||||
const receiver = gift ? gift.member : user;
|
||||
const receiverCanGetGems = await receiver.canGetGems();
|
||||
if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language));
|
||||
}
|
||||
|
||||
response = await stripeApi.charges.create({
|
||||
amount,
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
await payments.createSubscription({
|
||||
user,
|
||||
customerId: response.id,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
} else {
|
||||
let method = 'buyGems';
|
||||
let data = {
|
||||
user,
|
||||
customerId: response.id,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
gift,
|
||||
};
|
||||
|
||||
if (gift) {
|
||||
if (gift.type === 'subscription') method = 'createSubscription';
|
||||
data.paymentMethod = 'Gift';
|
||||
}
|
||||
|
||||
await payments[method](data);
|
||||
}
|
||||
};
|
||||
api.checkout = checkout;
|
||||
|
||||
/**
|
||||
* Edits a subscription created by Stripe
|
||||
@@ -176,7 +55,7 @@ api.editSubscription = async function editSubscription (options, stripeInc) {
|
||||
let customerId;
|
||||
|
||||
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
|
||||
let stripeApi = stripe;
|
||||
let stripeApi = getStripeApi();
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
if (groupId) {
|
||||
@@ -220,7 +99,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
|
||||
let customerId;
|
||||
|
||||
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
|
||||
let stripeApi = stripe;
|
||||
let stripeApi = getStripeApi();
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
if (groupId) {
|
||||
@@ -271,7 +150,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
|
||||
};
|
||||
|
||||
api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) {
|
||||
let stripeApi = stripe;
|
||||
let stripeApi = getStripeApi();
|
||||
let plan = shared.content.subscriptionBlocks.group_monthly;
|
||||
|
||||
await stripeApi.subscriptions.update(
|
||||
@@ -298,7 +177,7 @@ api.handleWebhooks = async function handleWebhooks (options, stripeInc) {
|
||||
let {requestBody} = options;
|
||||
|
||||
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
|
||||
let stripeApi = stripe;
|
||||
let stripeApi = getStripeApi();
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
// Verify the event by fetching it from Stripe
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import stripeModule from 'stripe';
|
||||
import nconf from 'nconf';
|
||||
|
||||
let stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
||||
|
||||
function setStripeApi (stripeInc) {
|
||||
stripe = stripeInc;
|
||||
}
|
||||
|
||||
function getStripeApi () {
|
||||
return stripe;
|
||||
}
|
||||
|
||||
module.exports = { getStripeApi, setStripeApi };
|
||||
@@ -0,0 +1,146 @@
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import { getStripeApi } from './api';
|
||||
import { model as User } from '../../../models/user';
|
||||
import { model as Coupon } from '../../../models/coupon';
|
||||
import {
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../../models/group';
|
||||
import shared from '../../../../common';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../errors';
|
||||
import payments from './../payments';
|
||||
import stripeConstants from './constants';
|
||||
|
||||
function getGiftAmount (gift) {
|
||||
if (gift.type === 'subscription') {
|
||||
return `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
|
||||
}
|
||||
|
||||
if (gift.gems.amount <= 0) {
|
||||
throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase'));
|
||||
}
|
||||
|
||||
return `${gift.gems.amount / 4 * 100}`;
|
||||
}
|
||||
|
||||
async function buyGems (gift, user, token, stripeApi) {
|
||||
let amount = 500; // $5
|
||||
|
||||
if (gift) amount = getGiftAmount(gift);
|
||||
|
||||
if (!gift || gift.type === 'gems') {
|
||||
const receiver = gift ? gift.member : user;
|
||||
const receiverCanGetGems = await receiver.canGetGems();
|
||||
if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language));
|
||||
}
|
||||
|
||||
const response = await stripeApi.charges.create({
|
||||
amount,
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function buySubscription (sub, coupon, email, user, token, groupId, stripeApi) {
|
||||
if (sub.discount) {
|
||||
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
|
||||
coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
|
||||
if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon'));
|
||||
}
|
||||
|
||||
let customerObject = {
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
};
|
||||
|
||||
if (groupId) {
|
||||
customerObject.quantity = sub.quantity;
|
||||
const groupFields = basicGroupFields.concat(' purchased');
|
||||
const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||
const membersCount = await group.getMemberCount();
|
||||
customerObject.quantity = membersCount + sub.quantity - 1;
|
||||
}
|
||||
|
||||
const response = await stripeApi.customers.create(customerObject);
|
||||
|
||||
let subscriptionId;
|
||||
if (groupId) subscriptionId = response.subscriptions.data[0].id;
|
||||
|
||||
return { subResponse: response, subId: subscriptionId };
|
||||
}
|
||||
|
||||
async function applyGemPayment (user, response, gift) {
|
||||
let method = 'buyGems';
|
||||
const data = {
|
||||
user,
|
||||
customerId: response.id,
|
||||
paymentMethod: stripeConstants.PAYMENT_METHOD,
|
||||
gift,
|
||||
};
|
||||
|
||||
if (gift) {
|
||||
if (gift.type === 'subscription') method = 'createSubscription';
|
||||
data.paymentMethod = 'Gift';
|
||||
}
|
||||
|
||||
await payments[method](data);
|
||||
}
|
||||
|
||||
async function checkout (options, stripeInc) {
|
||||
let {
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
} = options;
|
||||
let response;
|
||||
let subscriptionId;
|
||||
|
||||
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
|
||||
let stripeApi = getStripeApi();
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
if (!token) throw new BadRequest('Missing req.body.id');
|
||||
|
||||
if (gift) {
|
||||
const member = await User.findById(gift.uuid).exec();
|
||||
gift.member = member;
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
const { subId, subResponse } = await buySubscription(sub, coupon, email, user, token, groupId, stripeApi);
|
||||
subscriptionId = subId;
|
||||
response = subResponse;
|
||||
} else {
|
||||
response = await buyGems(gift, user, token, stripeApi);
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
await payments.createSubscription({
|
||||
user,
|
||||
customerId: response.id,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await applyGemPayment(user, response, gift);
|
||||
}
|
||||
|
||||
module.exports = { checkout };
|
||||
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
// CURRENCY_CODE: 'USD',
|
||||
// SELLER_NOTE: 'Habitica Payment',
|
||||
// SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription',
|
||||
// SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment',
|
||||
// STORE_NAME: 'Habitica',
|
||||
//
|
||||
// GIFT_TYPE_GEMS: 'gems',
|
||||
// GIFT_TYPE_SUBSCRIPTION: 'subscription',
|
||||
//
|
||||
// METHOD_BUY_GEMS: 'buyGems',
|
||||
// METHOD_CREATE_SUBSCRIPTION: 'createSubscription',
|
||||
PAYMENT_METHOD: 'Stripe',
|
||||
// PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)',
|
||||
};
|
||||
@@ -185,7 +185,7 @@ async function createSubscription (data) {
|
||||
senderMsg += ` ${data.gift.message}`;
|
||||
}
|
||||
|
||||
data.user.sendMessage(data.gift.member, { receiverMsg, senderMsg });
|
||||
data.user.sendMessage(data.gift.member, { receiverMsg, senderMsg, save: false });
|
||||
|
||||
if (data.gift.member.preferences.emailNotifications.giftedSubscription !== false) {
|
||||
txnEmail(data.gift.member, 'gifted-subscription', [
|
||||
@@ -208,12 +208,8 @@ async function createSubscription (data) {
|
||||
}
|
||||
}
|
||||
|
||||
if (group) {
|
||||
await group.save();
|
||||
} else {
|
||||
await data.user.save();
|
||||
}
|
||||
|
||||
if (group) await group.save();
|
||||
if (data.user && data.user.isModified()) await data.user.save();
|
||||
if (data.gift) await data.gift.member.save();
|
||||
|
||||
slack.sendSubscriptionNotification({
|
||||
|
||||
@@ -9,10 +9,13 @@ import {
|
||||
let _wrapAsyncFn = fn => (...args) => fn(...args).catch(args[2]);
|
||||
let noop = (req, res, next) => next();
|
||||
|
||||
module.exports.readController = function readController (router, controller) {
|
||||
module.exports.readController = function readController (router, controller, overrides = []) {
|
||||
_.each(controller, (action) => {
|
||||
let {method, url, middlewares = [], handler} = action;
|
||||
|
||||
// Allow to specify a list of routes (METHOD + URL) to skip
|
||||
if (overrides.indexOf(`${method}-${url}`) !== -1) return;
|
||||
|
||||
// If an authentication middleware is used run getUserLanguage after it, otherwise before
|
||||
// for cron instead use it only if an authentication middleware is present
|
||||
let authMiddlewareIndex = _.findIndex(middlewares, middleware => {
|
||||
@@ -44,7 +47,7 @@ module.exports.readController = function readController (router, controller) {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.walkControllers = function walkControllers (router, filePath) {
|
||||
module.exports.walkControllers = function walkControllers (router, filePath, overrides) {
|
||||
fs
|
||||
.readdirSync(filePath)
|
||||
.forEach(fileName => {
|
||||
@@ -52,7 +55,7 @@ module.exports.walkControllers = function walkControllers (router, filePath) {
|
||||
walkControllers(router, `${filePath}${fileName}/`);
|
||||
} else if (fileName.match(/\.js$/)) {
|
||||
let controller = require(filePath + fileName); // eslint-disable-line global-require
|
||||
module.exports.readController(router, controller);
|
||||
module.exports.readController(router, controller, overrides);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ passport.deserializeUser((obj, done) => done(null, obj));
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: nconf.get('FACEBOOK_KEY'),
|
||||
clientSecret: nconf.get('FACEBOOK_SECRET'),
|
||||
profileFields: ['id', 'email', 'displayName'],
|
||||
// callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
|
||||
}, (accessToken, refreshToken, profile, done) => done(null, profile)));
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IncomingWebhook } from '@slack/client';
|
||||
import logger from './logger';
|
||||
import { TAVERN_ID } from '../models/group';
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
|
||||
const SLACK_FLAGGING_URL = nconf.get('SLACK:FLAGGING_URL');
|
||||
const SLACK_FLAGGING_FOOTER_LINK = nconf.get('SLACK:FLAGGING_FOOTER_LINK');
|
||||
@@ -52,12 +53,14 @@ function sendFlagNotification ({
|
||||
authorName = `${message.user} - ${authorEmail} - ${message.uuid}`;
|
||||
}
|
||||
|
||||
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
|
||||
|
||||
flagSlack.send({
|
||||
text,
|
||||
attachments: [{
|
||||
fallback: 'Flag Message',
|
||||
color: 'danger',
|
||||
author_name: authorName,
|
||||
author_name: `${authorName}\n${timestamp}`,
|
||||
title,
|
||||
title_link: titleLink,
|
||||
text: message.text,
|
||||
|
||||
@@ -2,6 +2,9 @@ import got from 'got';
|
||||
import { isURL } from 'validator';
|
||||
import logger from './logger';
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
model as User,
|
||||
} from '../models/user';
|
||||
|
||||
const IS_PRODUCTION = nconf.get('IS_PROD');
|
||||
|
||||
@@ -72,7 +75,7 @@ export let taskScoredWebhook = new WebhookSender({
|
||||
transformData (data) {
|
||||
let { user, task, direction, delta } = data;
|
||||
|
||||
let extendedStats = user.addComputedStatsToJSONObj(user.stats.toJSON());
|
||||
let extendedStats = User.addComputedStatsToJSONObj(user.stats.toJSON(), user);
|
||||
|
||||
let userData = {
|
||||
// _id: user._id, added automatically when the webhook is sent
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import express from 'express';
|
||||
import expressValidator from 'express-validator';
|
||||
import analytics from './analytics';
|
||||
import setupBody from './setupBody';
|
||||
import routes from '../libs/routes';
|
||||
import path from 'path';
|
||||
|
||||
const API_V3_CONTROLLERS_PATH = path.join(__dirname, '/../controllers/api-v3/');
|
||||
const API_V4_CONTROLLERS_PATH = path.join(__dirname, '/../controllers/api-v4/');
|
||||
const TOP_LEVEL_CONTROLLERS_PATH = path.join(__dirname, '/../controllers/top-level/');
|
||||
|
||||
const app = express();
|
||||
|
||||
// re-set the view options because they are not inherited from the top level app
|
||||
app.set('view engine', 'pug');
|
||||
app.set('views', `${__dirname}/../../views`);
|
||||
|
||||
app.use(expressValidator());
|
||||
app.use(analytics);
|
||||
app.use(setupBody);
|
||||
|
||||
const topLevelRouter = express.Router(); // eslint-disable-line new-cap
|
||||
|
||||
routes.walkControllers(topLevelRouter, TOP_LEVEL_CONTROLLERS_PATH);
|
||||
app.use('/', topLevelRouter);
|
||||
|
||||
const v3Router = express.Router(); // eslint-disable-line new-cap
|
||||
routes.walkControllers(v3Router, API_V3_CONTROLLERS_PATH);
|
||||
app.use('/api/v3', v3Router);
|
||||
|
||||
// API v4 proxies API v3 routes by default.
|
||||
// It can also disable or override v3 routes
|
||||
|
||||
// A list of v3 routes in the format METHOD-URL to skip
|
||||
const v4RouterOverrides = [
|
||||
// 'GET-/status', Example to override the GET /status api call
|
||||
];
|
||||
|
||||
const v4Router = express.Router(); // eslint-disable-line new-cap
|
||||
routes.walkControllers(v4Router, API_V3_CONTROLLERS_PATH, v4RouterOverrides);
|
||||
routes.walkControllers(v4Router, API_V4_CONTROLLERS_PATH);
|
||||
app.use('/api/v4', v4Router);
|
||||
|
||||
module.exports = app;
|
||||
@@ -23,7 +23,6 @@ async function checkForActiveCron (user, now) {
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature,
|
||||
lastCron: now, // setting lastCron now so we don't risk re-running parts of cron if it fails
|
||||
'auth.timestamps.loggedin': now,
|
||||
},
|
||||
}).exec();
|
||||
@@ -35,6 +34,14 @@ async function checkForActiveCron (user, now) {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLastCron (user, now) {
|
||||
await User.update({
|
||||
_id: user._id,
|
||||
}, {
|
||||
lastCron: now, // setting lastCron now so we don't risk re-running parts of cron if it fails
|
||||
}).exec();
|
||||
}
|
||||
|
||||
async function unlockUser (user) {
|
||||
await User.update({
|
||||
_id: user._id,
|
||||
@@ -51,9 +58,12 @@ async function cronAsync (req, res) {
|
||||
let now = new Date();
|
||||
|
||||
try {
|
||||
await checkForActiveCron(user, now);
|
||||
|
||||
user = res.locals.user = await User.findOne({_id: user._id}).exec();
|
||||
let {daysMissed, timezoneOffsetFromUserPrefs} = user.daysUserHasMissed(now, req);
|
||||
|
||||
await checkForActiveCron(user, now);
|
||||
await updateLastCron(user, now);
|
||||
|
||||
if (daysMissed <= 0) {
|
||||
if (user.isModified()) await user.save();
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from './redirects';
|
||||
import v1 from './v1';
|
||||
import v2 from './v2';
|
||||
import v3 from './v3';
|
||||
import appRoutes from './appRoutes';
|
||||
import responseHandler from './response';
|
||||
import {
|
||||
attachTranslateFunction,
|
||||
@@ -89,12 +89,11 @@ module.exports = function attachMiddlewares (app, server) {
|
||||
}
|
||||
app.use('/api/v2', v2);
|
||||
app.use('/api/v1', v1);
|
||||
app.use(v3); // the main app, also setup top-level routes
|
||||
app.use(appRoutes); // the main app, also setup top-level routes
|
||||
staticMiddleware(app);
|
||||
|
||||
app.use(notFoundHandler);
|
||||
|
||||
// Error handler middleware, define as the last one.
|
||||
// Used for v3 and v1, v2 will keep using its own error handler
|
||||
app.use(errorHandler);
|
||||
};
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import express from 'express';
|
||||
import expressValidator from 'express-validator';
|
||||
import analytics from './analytics';
|
||||
import setupBody from './setupBody';
|
||||
import routes from '../libs/routes';
|
||||
import path from 'path';
|
||||
|
||||
const API_CONTROLLERS_PATH = path.join(__dirname, '/../controllers/api-v3/');
|
||||
const TOP_LEVEL_CONTROLLERS_PATH = path.join(__dirname, '/../controllers/top-level/');
|
||||
|
||||
const v3app = express();
|
||||
|
||||
// re-set the view options because they are not inherited from the top level app
|
||||
v3app.set('view engine', 'pug');
|
||||
v3app.set('views', `${__dirname}/../../views`);
|
||||
|
||||
v3app.use(expressValidator());
|
||||
v3app.use(analytics);
|
||||
v3app.use(setupBody);
|
||||
|
||||
const topLevelRouter = express.Router(); // eslint-disable-line new-cap
|
||||
|
||||
routes.walkControllers(topLevelRouter, TOP_LEVEL_CONTROLLERS_PATH);
|
||||
v3app.use('/', topLevelRouter);
|
||||
|
||||
const v3Router = express.Router(); // eslint-disable-line new-cap
|
||||
routes.walkControllers(v3Router, API_CONTROLLERS_PATH);
|
||||
v3app.use('/api/v3', v3Router);
|
||||
|
||||
module.exports = v3app;
|
||||
@@ -33,7 +33,7 @@ let schema = new Schema({
|
||||
},
|
||||
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
||||
group: {type: String, ref: 'Group', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
||||
memberCount: {type: Number, default: 1},
|
||||
memberCount: {type: Number, default: 0},
|
||||
prize: {type: Number, default: 0, min: 0},
|
||||
categories: [{
|
||||
slug: {type: String},
|
||||
|
||||
@@ -171,29 +171,11 @@ function _setUpNewUser (user) {
|
||||
return _populateDefaultTasks(user, taskTypes);
|
||||
}
|
||||
|
||||
function _getFacebookName (fb) {
|
||||
if (!fb) {
|
||||
return;
|
||||
}
|
||||
let possibleName = fb.displayName || fb.name || fb.username;
|
||||
|
||||
if (possibleName) {
|
||||
return possibleName;
|
||||
}
|
||||
|
||||
if (fb.first_name && fb.last_name) {
|
||||
return `${fb.first_name} ${fb.last_name}`;
|
||||
}
|
||||
}
|
||||
|
||||
function _setProfileName (user) {
|
||||
let google = user.auth.google;
|
||||
|
||||
let localUsername = user.auth.local && user.auth.local.username;
|
||||
let googleUsername = google && google.displayName;
|
||||
let anonymous = 'profile name not found';
|
||||
|
||||
return localUsername || _getFacebookName(user.auth.facebook) || googleUsername || anonymous;
|
||||
return localUsername || anonymous;
|
||||
}
|
||||
|
||||
schema.pre('validate', function preValidateUser (next) {
|
||||
|
||||
@@ -99,6 +99,8 @@ schema.methods.getObjectionsToInteraction = function getObjectionsToInteraction
|
||||
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, options) {
|
||||
let sender = this;
|
||||
let senderMsg = options.senderMsg || options.receiverMsg;
|
||||
// whether to save users after sending the message, defaults to true
|
||||
let saveUsers = options.save === false ? false : true;
|
||||
|
||||
common.refPush(userToReceiveMessage.inbox.messages, chatDefaults(options.receiverMsg, sender));
|
||||
userToReceiveMessage.inbox.newMessages++;
|
||||
@@ -130,8 +132,9 @@ schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, o
|
||||
common.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(senderMsg, userToReceiveMessage)));
|
||||
sender.markModified('inbox.messages');
|
||||
|
||||
let promises = [userToReceiveMessage.save(), sender.save()];
|
||||
await Promise.all(promises);
|
||||
if (saveUsers) {
|
||||
await Promise.all([userToReceiveMessage.save(), sender.save()]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -171,17 +174,27 @@ schema.statics.pushNotification = async function pushNotification (query, type,
|
||||
await this.update(query, {$push: {notifications: newNotification.toObject()}}, {multi: true}).exec();
|
||||
};
|
||||
|
||||
// Static method to add/remove properties to a JSON User object,
|
||||
// For example for when the user is returned using `.lean()` and thus doesn't
|
||||
// have access to any mongoose helper
|
||||
schema.statics.transformJSONUser = function transformJSONUser (jsonUser, addComputedStats = false) {
|
||||
// Add id property
|
||||
jsonUser.id = jsonUser._id;
|
||||
|
||||
if (addComputedStats) this.addComputedStatsToJSONObj(jsonUser.stats, jsonUser);
|
||||
};
|
||||
|
||||
// Add stats.toNextLevel, stats.maxMP and stats.maxHealth
|
||||
// to a JSONified User stats object
|
||||
schema.methods.addComputedStatsToJSONObj = function addComputedStatsToUserJSONObj (statsObject) {
|
||||
schema.statics.addComputedStatsToJSONObj = function addComputedStatsToUserJSONObj (userStatsJSON, user) {
|
||||
// NOTE: if an item is manually added to this.stats then
|
||||
// common/fns/predictableRandom must be tweaked so the new item is not considered.
|
||||
// Otherwise the client will have it while the server won't and the results will be different.
|
||||
statsObject.toNextLevel = common.tnl(this.stats.lvl);
|
||||
statsObject.maxHealth = common.maxHealth;
|
||||
statsObject.maxMP = common.statsComputed(this).maxMP;
|
||||
userStatsJSON.toNextLevel = common.tnl(user.stats.lvl);
|
||||
userStatsJSON.maxHealth = common.maxHealth;
|
||||
userStatsJSON.maxMP = common.statsComputed(user).maxMP;
|
||||
|
||||
return statsObject;
|
||||
return userStatsJSON;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -252,7 +252,7 @@ let schema = new Schema({
|
||||
gear: {
|
||||
owned: _.transform(shared.content.gear.flat, (m, v) => {
|
||||
m[v.key] = {type: Boolean};
|
||||
if (v.key.match(/(armor|head|shield)_warrior_0/) || v.gearSet === 'glasses') {
|
||||
if (v.key.match(/(armor|head|shield)_warrior_0/) || v.gearSet === 'glasses' || v.gearSet === 'headband') {
|
||||
m[v.key].default = true;
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -20,7 +20,7 @@ const app = express();
|
||||
app.set('port', nconf.get('PORT'));
|
||||
|
||||
// Setup translations
|
||||
// Must come before attach middlwares so Mongoose validations can use translations
|
||||
// Must come before attach middlewares so Mongoose validations can use translations
|
||||
import './libs/i18n';
|
||||
|
||||
import attachMiddlewares from './middlewares/index';
|
||||
|
||||
Reference in New Issue
Block a user