mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-09 11:11:17 -05:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad8d0a3ed5 | |||
| ac322b1925 | |||
| ff46459159 | |||
| e952a1dbaf | |||
| e64428c89e | |||
| b97dfdfa83 | |||
| e1a68cd02a | |||
| 5d7a3bedf7 | |||
| 55cdd19215 | |||
| ce24ce3079 |
+1
-1
Submodule habitica-images updated: 359153997e...8a96a0ff62
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.45.0",
|
||||
"version": "5.44.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.45.0",
|
||||
"version": "5.44.2",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.45.0",
|
||||
"version": "5.44.2",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
xdescribe('GET /export/avatar-:memberId.html', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId', async () => {
|
||||
await expect(user.get('/export/avatar-:memberId.html')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
const dummyId = generateUUID();
|
||||
await expect(user.get(`/export/avatar-${dummyId}.html`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', { userId: dummyId }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an html page', async () => {
|
||||
const res = await user.get(`/export/avatar-${user._id}.html`);
|
||||
expect(res.substring(0, 100).indexOf('<!DOCTYPE html>')).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
// TODO how to test this route since it points to a file on AWS s3?
|
||||
|
||||
describe('GET /export/avatar-:memberId.png', () => {});
|
||||
@@ -498,13 +498,8 @@ export default {
|
||||
|
||||
await this.triggerGetWorldState();
|
||||
this.currentEvent = _find(this.currentEventList, event => Boolean(event.season));
|
||||
if (this.currentEvent.season === 'valentines') {
|
||||
this.imageURLs.background = 'url(/static/npc/spring/seasonal_shop_opened_background.png)';
|
||||
this.imageURLs.npc = 'url(/static/npc/spring/seasonal_shop_opened_npc.png)';
|
||||
} else {
|
||||
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
|
||||
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
|
||||
}
|
||||
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
|
||||
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
|
||||
@@ -348,6 +348,7 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import draggable from 'vuedraggable';
|
||||
import { shouldDo } from '@/../../common/script/cron';
|
||||
import inAppRewards from '@/../../common/script/libs/inAppRewards';
|
||||
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
import Task from './task';
|
||||
@@ -481,10 +482,25 @@ export default {
|
||||
return this.$t('addATask', { type });
|
||||
},
|
||||
badgeCount () {
|
||||
if (this.type === 'reward') {
|
||||
return 0;
|
||||
// 0 means the badge will not be shown
|
||||
// It is shown for the all and due views of dailies
|
||||
// and for the active and scheduled views of todos.
|
||||
if (this.type === 'todo' && this.activeFilter.label !== 'complete2') {
|
||||
return this.taskList.length;
|
||||
} if (this.type === 'daily') {
|
||||
if (this.activeFilter.label === 'due') {
|
||||
return this.taskList.length;
|
||||
} if (this.activeFilter.label === 'all') {
|
||||
return this.taskList
|
||||
.reduce(
|
||||
(count, t) => (!t.completed
|
||||
&& shouldDo(new Date(), t, this.getUserPreferences) ? count + 1 : count),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
return this.taskList.length;
|
||||
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -1340,7 +1340,7 @@ export default {
|
||||
},
|
||||
|
||||
openAdminPanel () {
|
||||
this.$router.push(`/admin/panel/${this.hero._id}`);
|
||||
this.$router.push(`/admin-panel/${this.hero._id}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<strong>{{ $t('equipment') }}:</strong>
|
||||
<span :class="{ 'positive-stat': statsComputed.gearBonus[stat] !== 0 }">
|
||||
{{ statsComputed.gearBonus[stat] !== 0 ? '+' : '' }}{{
|
||||
statsComputed.gearBonus[stat] + statsComputed.classBonus[stat]
|
||||
statsComputed.gearBonus[stat]
|
||||
}}
|
||||
</span>
|
||||
</li>
|
||||
@@ -246,9 +246,7 @@
|
||||
:class="{white: user.preferences.background}"
|
||||
style="overflow:hidden"
|
||||
>
|
||||
<Sprite
|
||||
v-if="user.preferences.background && user.preferences.background !== ''"
|
||||
:image-name="'icon_background_' + user.preferences.background" />
|
||||
<Sprite :image-name="'icon_background_' + user.preferences.background" />
|
||||
</div>
|
||||
<b-popover
|
||||
v-if="label !== 'skip'
|
||||
|
||||
@@ -122,7 +122,7 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
experimentalMinChunkSize: 20000
|
||||
experimentalMinChunkSize: 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1353,6 +1353,8 @@ api.getLookingForParty = {
|
||||
|
||||
const seekers = await User
|
||||
.find({
|
||||
'auth.blocked': { $ne: true },
|
||||
'flags.chatRevoked': { $ne: true },
|
||||
'party.seeking': { $exists: true },
|
||||
'invitations.party.id': { $exists: false },
|
||||
'auth.timestamps.loggedin': {
|
||||
@@ -1360,12 +1362,13 @@ api.getLookingForParty = {
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line no-multi-str
|
||||
.select('_id auth.blocked auth.local.username auth.timestamps backer contributor.level \
|
||||
flags.chatRevoked flags.classSelected inbox.blocks invitations.party items.gear.costume \
|
||||
.select('_id auth.local.username auth.timestamps backer contributor.level \
|
||||
flags.classSelected inbox.blocks invitations.party items.gear.costume \
|
||||
items.gear.equipped loginIncentives party._id preferences.background preferences.chair \
|
||||
preferences.costume preferences.hair preferences.shirt preferences.size preferences.skin \
|
||||
preferences.language profile.name stats.buffs stats.class stats.lvl')
|
||||
.sort('-auth.timestamps.loggedin')
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
const filteredSeekers = seekers.filter(seeker => {
|
||||
|
||||
@@ -24,7 +24,7 @@ const api = {};
|
||||
api.getInboxMessages = {
|
||||
method: 'GET',
|
||||
url: '/inbox/messages',
|
||||
middlewares: [authWithHeaders({ userFieldsToInclude: ['profile', 'contributor', 'backer', 'inbox'] })],
|
||||
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['profile', 'contributor', 'backer', 'inbox'] })],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
const { page } = req.query;
|
||||
|
||||
@@ -40,7 +40,7 @@ const api = {};
|
||||
api.createTag = {
|
||||
method: 'POST',
|
||||
url: '/tags',
|
||||
middlewares: [authWithHeaders()],
|
||||
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -69,7 +69,7 @@ api.createTag = {
|
||||
api.getTags = {
|
||||
method: 'GET',
|
||||
url: '/tags',
|
||||
middlewares: [authWithHeaders()],
|
||||
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['tags'] })],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
res.respond(200, user.tags);
|
||||
@@ -95,7 +95,7 @@ api.getTags = {
|
||||
api.getTag = {
|
||||
method: 'GET',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders()],
|
||||
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['tags'] })],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -133,7 +133,7 @@ api.getTag = {
|
||||
api.updateTag = {
|
||||
method: 'PUT',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders()],
|
||||
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -175,7 +175,7 @@ api.updateTag = {
|
||||
api.reorderTags = {
|
||||
method: 'POST',
|
||||
url: '/reorder-tags',
|
||||
middlewares: [authWithHeaders()],
|
||||
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -215,7 +215,7 @@ api.reorderTags = {
|
||||
api.deleteTag = {
|
||||
method: 'DELETE',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders()],
|
||||
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ api.getUserTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/user',
|
||||
middlewares: [authWithHeaders({
|
||||
// Some fields (including _id, preferences) are always loaded (see middlewares/auth)
|
||||
leanUser: true,
|
||||
userFieldsToInclude: ['tasksOrder'],
|
||||
})],
|
||||
async handler (req, res) {
|
||||
@@ -953,7 +953,7 @@ api.addChecklistItem = {
|
||||
api.scoreCheckListItem = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/checklist/:itemId/score',
|
||||
middlewares: [authWithHeaders()],
|
||||
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['_id'] })],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ api.getUserAnonymized = {
|
||||
{ type: { $in: ['habit', 'daily', 'reward'] } },
|
||||
],
|
||||
};
|
||||
const tasks = await Tasks.Task.find(query).exec();
|
||||
const tasks = await Tasks.Task.find(query).lean().exec();
|
||||
|
||||
forEach(tasks, task => {
|
||||
task.text = 'task text';
|
||||
|
||||
@@ -22,6 +22,7 @@ api.purchaseHistory = {
|
||||
let transactions = await Transaction
|
||||
.find({ userId: req.params.memberId })
|
||||
.sort({ createdAt: -1 })
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (!res.locals.user.hasPermission('userSupport')) {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import {
|
||||
disableCache,
|
||||
} from '../../middlewares/cache';
|
||||
import SERVER_STATUS from '../../libs/serverStatus';
|
||||
|
||||
const api = {};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/ready Get Habitica's Server readiness status
|
||||
* @apiName GetReady
|
||||
* @apiGroup Status
|
||||
*
|
||||
* @apiSuccess {String} data.status 'ready' if everything is ok
|
||||
*
|
||||
* @apiSuccessExample {JSON} Server is Ready
|
||||
* {
|
||||
* 'status': 'ready',
|
||||
* }
|
||||
*/
|
||||
api.getReady = {
|
||||
method: 'GET',
|
||||
url: '/ready',
|
||||
// explicitly disable caching so that the server is always checked
|
||||
middlewares: [disableCache],
|
||||
async handler (req, res) {
|
||||
// This allows kubernetes to determine if the server is ready to receive traffic
|
||||
if (!SERVER_STATUS.MONGODB || !SERVER_STATUS.REDIS || !SERVER_STATUS.EXPRESS) {
|
||||
res.respond(503, {
|
||||
status: 'not ready',
|
||||
});
|
||||
} else {
|
||||
res.respond(200, {
|
||||
status: 'ready',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
@@ -313,7 +313,9 @@ api.purchaseHistory = {
|
||||
url: '/user/purchase-history',
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
const transactions = await Transaction.find({ userId: user._id }).sort({ createdAt: -1 });
|
||||
const transactions = await Transaction.find({ userId: user._id })
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
res.respond(200, transactions);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,10 +6,18 @@ import moment from 'moment';
|
||||
import md from 'habitica-markdown';
|
||||
import csvStringify from '../../libs/csvStringify';
|
||||
import { marshallUserData } from '../../libs/xmlMarshaller';
|
||||
import { NotFound } from '../../libs/errors';
|
||||
import * as Tasks from '../../models/task';
|
||||
import * as inboxLib from '../../libs/inbox';
|
||||
// import { model as User } from '../../models/user';
|
||||
import { authWithSession } from '../../middlewares/auth';
|
||||
/* import {
|
||||
S3,
|
||||
} from '../../libs/aws'; */
|
||||
|
||||
// const S3_BUCKET = nconf.get('S3_BUCKET');
|
||||
|
||||
// const BASE_URL = nconf.get('BASE_URL');
|
||||
|
||||
const api = {};
|
||||
|
||||
@@ -45,7 +53,7 @@ api.exportUserHistory = {
|
||||
const tasks = await Tasks.Task.find({
|
||||
userId: user._id,
|
||||
type: { $in: ['habit', 'daily'] },
|
||||
}).exec();
|
||||
}).lean().exec();
|
||||
|
||||
const output = [
|
||||
['Task Name', 'Task ID', 'Task Type', 'Date', 'Value'],
|
||||
@@ -84,7 +92,7 @@ async function _getUserDataForExport (user) {
|
||||
const [tasks, messages] = await Promise.all([
|
||||
Tasks.Task.find({
|
||||
userId: user._id,
|
||||
}).exec(),
|
||||
}).lean().exec(),
|
||||
|
||||
inboxLib.getUserInbox(user, { asArray: false }),
|
||||
]);
|
||||
@@ -92,7 +100,6 @@ async function _getUserDataForExport (user) {
|
||||
userData.inbox.messages = messages;
|
||||
|
||||
_(tasks)
|
||||
.map(task => task.toJSON())
|
||||
.groupBy(task => task.type)
|
||||
.forEach((tasksPerType, taskType) => {
|
||||
userData.tasks[`${taskType}s`] = tasksPerType;
|
||||
@@ -149,6 +156,122 @@ api.exportUserDataXml = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /export/avatar-:uuid.html Render a user avatar as an HTML page
|
||||
* @apiName ExportUserAvatarHtml
|
||||
* @apiDescription This HTML export feature is not currently working (https://github.com/HabitRPG/habitica/issues/9489).
|
||||
* @apiGroup DataExport
|
||||
*
|
||||
* @apiParam (Path) {String} uuid The User ID of the user
|
||||
*
|
||||
* @apiSuccess {HTML} File An html page rendering the user's avatar.
|
||||
*
|
||||
* @apiUse UserNotFound
|
||||
*/
|
||||
// @TODO fix
|
||||
api.exportUserAvatarHtml = {
|
||||
method: 'GET',
|
||||
url: '/export/avatar-:memberId.html',
|
||||
// middlewares: [locals],
|
||||
async handler (/* req, res */) {
|
||||
throw new NotFound('This API route is currently not available. See https://github.com/HabitRPG/habitica/issues/9489.');
|
||||
|
||||
/* req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { memberId } = req.params;
|
||||
|
||||
throw new NotFound('This API route is currently not available. See https://github.com/HabitRPG/habitica/issues/9489.');
|
||||
|
||||
const member = await User
|
||||
.findById(memberId)
|
||||
.select('stats profile items achievements preferences backer contributor')
|
||||
.exec();
|
||||
|
||||
if (!member) throw new NotFound(res.t('userWithIDNotFound', { userId: memberId }));
|
||||
res.render('avatar-static', {
|
||||
title: member.profile.name,
|
||||
env: _.defaults({ user: member }, res.locals.habitrpg),
|
||||
}); */
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /export/avatar-:uuid.png Render a user avatar as a PNG file
|
||||
* @apiName ExportUserAvatarPng
|
||||
* @apiDescription This PNG export feature is not currently working (https://github.com/HabitRPG/habitica/issues/9489).
|
||||
* @apiGroup DataExport
|
||||
*
|
||||
* @apiParam (Path) {String} uuid The User ID of the user
|
||||
*
|
||||
* @apiSuccess {PNG} File A png file of the user's avatar.
|
||||
*/
|
||||
api.exportUserAvatarPng = {
|
||||
method: 'GET',
|
||||
url: '/export/avatar-:memberId.png',
|
||||
async handler (/* req, res */) {
|
||||
throw new NotFound('This API route is currently not available. See https://github.com/HabitRPG/habitica/issues/9489.');
|
||||
|
||||
/* req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { memberId } = req.params;
|
||||
|
||||
const filename = `avatars/${memberId}.png`;
|
||||
const s3url = `https://${S3_BUCKET}.s3.amazonaws.com/${filename}`;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await got.head(s3url); // TODO add timeout and retries
|
||||
} catch (gotError) {
|
||||
// If the file does not exist AWS S3 can return a 403 error
|
||||
if (gotError.code !== 'ENOTFOUND' && gotError.statusCode
|
||||
!== 404 && gotError.statusCode !== 403) {
|
||||
throw gotError;
|
||||
}
|
||||
}
|
||||
|
||||
// cache images for 30 minutes on aws, else upload a new one
|
||||
if (response && response.statusCode === 200 && moment()
|
||||
.diff(response.headers['last-modified'], 'minutes') < 30) {
|
||||
return res.redirect(s3url);
|
||||
}
|
||||
|
||||
const pageBuffer = await new Pageres()
|
||||
.src(`${BASE_URL}/export/avatar-${memberId}.html`, ['140x147'], {
|
||||
crop: true,
|
||||
filename: filename.replace('.png', ''),
|
||||
})
|
||||
.run();
|
||||
|
||||
const s3upload = S3.upload({
|
||||
Bucket: S3_BUCKET,
|
||||
Key: filename,
|
||||
ACL: 'public-read',
|
||||
StorageClass: 'REDUCED_REDUNDANCY',
|
||||
ContentType: 'image/png',
|
||||
Expires: moment().add({ minutes: 5 }).toDate(),
|
||||
Body: pageBuffer,
|
||||
});
|
||||
|
||||
const s3res = await new Promise((resolve, reject) => {
|
||||
s3upload.send((err, s3uploadRes) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(s3uploadRes);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return res.redirect(s3res.Location); */
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /export/inbox.html Export user private messages as HTML document
|
||||
* @apiName ExportUserPrivateMessages
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function sendChatPushNotifications (user, group, message, mentions,
|
||||
'party._id': group._id,
|
||||
_id: { $ne: user._id },
|
||||
})
|
||||
.lean()
|
||||
.select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username')
|
||||
.exec();
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ export async function getGroupChat (group, options = {}) {
|
||||
.sort('-timestamp');
|
||||
|
||||
if (before) {
|
||||
const beforeMessage = await Chat.findOne({ _id: before }).exec();
|
||||
const beforeMessage = await Chat.findOne({ _id: before }, { timestamp: 1 }).lean().exec();
|
||||
if (beforeMessage) {
|
||||
query = query.where('timestamp').lt(beforeMessage.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
const groupChat = await query.limit(effectiveLimit).exec();
|
||||
const groupChat = await query.limit(effectiveLimit).lean().exec();
|
||||
|
||||
// @TODO: Concat old chat to keep continuity of chat stored on group object
|
||||
const currentGroupChat = group.chat || [];
|
||||
|
||||
@@ -22,7 +22,7 @@ async function usersMapByConversations (users) {
|
||||
stats: 1,
|
||||
flags: 1,
|
||||
inbox: 1,
|
||||
}).exec();
|
||||
}).lean().exec();
|
||||
|
||||
for (const usr of loadedUsers) {
|
||||
const loadedUserConversation = {
|
||||
|
||||
@@ -3,7 +3,6 @@ import winston from 'winston';
|
||||
import { Loggly } from 'winston-loggly-bulk';
|
||||
import nconf from 'nconf';
|
||||
import _ from 'lodash';
|
||||
import os from 'os';
|
||||
import {
|
||||
CustomError,
|
||||
} from './errors';
|
||||
@@ -66,8 +65,9 @@ if (IS_PROD) {
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (LOGGLY_TOKEN && LOGGLY_SUBDOMAIN) {
|
||||
const tags = ['Winston-NodeJS', os.hostname()];
|
||||
const tags = ['Winston-NodeJS'];
|
||||
if (nconf.get('SERVER_EMOJI')) {
|
||||
tags.push(nconf.get('SERVER_EMOJI'));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
getDevelopmentConnectionUrl,
|
||||
getDefaultConnectionOptions,
|
||||
} from './mongodb';
|
||||
import SERVER_STATUS from './serverStatus';
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const MAINTENANCE_MODE = nconf.get('MAINTENANCE_MODE');
|
||||
@@ -25,13 +24,6 @@ const connectionUrl = IS_PROD ? DB_URI : getDevelopmentConnectionUrl(DB_URI);
|
||||
export default async function connectToMongoDB () {
|
||||
// Do not connect to MongoDB when in maintenance mode
|
||||
if (MAINTENANCE_MODE !== 'true') {
|
||||
mongoose.connection.on('open', () => {
|
||||
SERVER_STATUS.MONGODB = true;
|
||||
});
|
||||
mongoose.connection.on('disconnected', () => {
|
||||
SERVER_STATUS.MONGODB = false;
|
||||
});
|
||||
|
||||
return mongoose.connect(connectionUrl, mongooseOptions).then(() => {
|
||||
logger.info('Connected with Mongoose.');
|
||||
});
|
||||
|
||||
@@ -169,7 +169,10 @@ api.subscribe = async function subscribe (user, receipt, headers, nextPaymentPro
|
||||
{ 'purchased.plan.customerId': purchase.originalTransactionId },
|
||||
{ 'purchased.plan.customerId': purchase.transactionId },
|
||||
],
|
||||
}).exec();
|
||||
}, {
|
||||
_id: 1,
|
||||
'purchased.plan': 1,
|
||||
}).lean().exec();
|
||||
if (existingUsers.length > 0) {
|
||||
if (purchase.originalTransactionId === purchase.transactionId) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
const SERVER_STATUS = {
|
||||
MONGODB: false,
|
||||
REDIS: false,
|
||||
EXPRESS: false,
|
||||
};
|
||||
|
||||
export default SERVER_STATUS;
|
||||
@@ -178,7 +178,7 @@ async function getTasks (req, res, options = {}) {
|
||||
],
|
||||
},
|
||||
{ _id: 1 },
|
||||
).exec();
|
||||
).lean().exec();
|
||||
}
|
||||
if (upgradedGroups.length > 0) {
|
||||
for (const upgradedGroup of upgradedGroups) {
|
||||
@@ -270,7 +270,6 @@ async function getTasks (req, res, options = {}) {
|
||||
remove(taskOrder, taskId => tasks.findIndex(task => task._id === taskId) === -1);
|
||||
if (preLength !== taskOrder.length) {
|
||||
owner.tasksOrder[key] = taskOrder;
|
||||
owner.markModified('tasksOrder');
|
||||
ownerDirty = true;
|
||||
}
|
||||
});
|
||||
@@ -303,7 +302,17 @@ async function getTasks (req, res, options = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
if (ownerDirty) await owner.save();
|
||||
if (ownerDirty) {
|
||||
let model;
|
||||
if (challenge) {
|
||||
model = Challenge;
|
||||
} else if (group) {
|
||||
model = Group;
|
||||
} else {
|
||||
model = User;
|
||||
}
|
||||
await model.updateOne({ _id: owner._id }, { tasksOrder: owner.tasksOrder }).exec();
|
||||
}
|
||||
|
||||
// Remove empty values from the array and add any unordered task
|
||||
orderedTasks = compact(orderedTasks).concat(unorderedTasks);
|
||||
|
||||
@@ -82,7 +82,7 @@ export function setNextDue (task, user, dueDateOption) {
|
||||
now = dateTaskIsDue;
|
||||
}
|
||||
|
||||
const optionsForShouldDo = user.preferences.toObject();
|
||||
const optionsForShouldDo = user.preferences;
|
||||
optionsForShouldDo.now = now;
|
||||
task.isDue = shared.shouldDo(dateTaskIsDue, task, optionsForShouldDo);
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ export async function update (req, res, { isV3 = false }) {
|
||||
],
|
||||
}, {
|
||||
_id: 1,
|
||||
}).exec();
|
||||
}).lean().exec();
|
||||
|
||||
matchingGroupsArray = _.map(matchingGroups, groupRecord => groupRecord._id);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from '../libs/errors';
|
||||
import logger from '../libs/logger';
|
||||
import { apiError } from '../libs/apiError';
|
||||
import SERVER_STATUS from '../libs/serverStatus';
|
||||
|
||||
// Middleware to rate limit requests to the API
|
||||
|
||||
@@ -48,14 +47,6 @@ if (RATE_LIMITER_ENABLED) {
|
||||
enable_offline_queue: false,
|
||||
});
|
||||
|
||||
redisClient.on('ready', () => {
|
||||
SERVER_STATUS.REDIS = true;
|
||||
});
|
||||
|
||||
redisClient.on('reconnecting', () => {
|
||||
SERVER_STATUS.REDIS = false;
|
||||
});
|
||||
|
||||
redisClient.on('error', error => {
|
||||
logger.error(error, 'Redis Error');
|
||||
});
|
||||
@@ -65,8 +56,6 @@ if (RATE_LIMITER_ENABLED) {
|
||||
storeClient: redisClient,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
SERVER_STATUS.REDIS = true;
|
||||
}
|
||||
|
||||
function setResponseHeaders (res, rateLimiterRes) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const logRequestData = (req, res, next) => {
|
||||
|
||||
export const logSlowRequests = (req, res, next) => {
|
||||
req.requestStartTime = Date.now();
|
||||
req.once('close', () => {
|
||||
req.on('close', () => {
|
||||
const requestTime = Date.now() - req.requestStartTime;
|
||||
if (requestTime > SLOW_REQUEST_THRESHOLD) {
|
||||
const data = buildBaseLogData(req);
|
||||
|
||||
@@ -43,6 +43,7 @@ schema.statics.getNews = async function getNews (isAdmin, options = { page: 0 })
|
||||
.sort({ publishDate: -1 })
|
||||
.limit(POSTS_PER_PAGE)
|
||||
.skip(POSTS_PER_PAGE * Number(page))
|
||||
.lean()
|
||||
.exec();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import logger from '../../libs/logger';
|
||||
import schema from './schema'; // eslint-disable-line import/no-cycle
|
||||
|
||||
import './hooks'; // eslint-disable-line import/no-cycle
|
||||
@@ -19,19 +18,3 @@ export const nameFields = 'profile.name auth.local.username flags.verifiedUserna
|
||||
export { schema };
|
||||
|
||||
export const model = mongoose.model('User', schema);
|
||||
|
||||
// Initially export an empty object so external requires will get
|
||||
// the right object by reference when it's defined later
|
||||
// Otherwise it would remain undefined if requested before the query executes
|
||||
export const mods = [];
|
||||
|
||||
mongoose.model('User')
|
||||
.find({ 'contributor.moderator': true })
|
||||
.sort('-contributor.level -backer.npc profile.name')
|
||||
.select('profile contributor backer')
|
||||
.exec()
|
||||
.then(foundMods => {
|
||||
// Using push to maintain the reference to mods
|
||||
mods.push(...foundMods);
|
||||
})
|
||||
.catch(err => logger.error(err));
|
||||
|
||||
@@ -367,14 +367,14 @@ schema.methods.getUtcOffset = function getUtcOffset () {
|
||||
return common.fns.getUtcOffset(this);
|
||||
};
|
||||
|
||||
schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
schema.statics.daysUserHasMissed = function daysUserHasMissed (user, now, req = {}) {
|
||||
// If the user's timezone has changed (due to travel or daylight savings),
|
||||
// cron can be triggered twice in one day, so we check for that and use
|
||||
// both timezones to work out if cron should run.
|
||||
// CDS = Custom Day Start time.
|
||||
let timezoneUtcOffsetFromUserPrefs = this.getUtcOffset();
|
||||
const timezoneUtcOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron)
|
||||
? -this.preferences.timezoneOffsetAtLastCron
|
||||
let timezoneUtcOffsetFromUserPrefs = common.fns.getUtcOffset(user);
|
||||
const timezoneUtcOffsetAtLastCron = Number.isFinite(user.preferences.timezoneOffsetAtLastCron)
|
||||
? -user.preferences.timezoneOffsetAtLastCron
|
||||
: timezoneUtcOffsetFromUserPrefs;
|
||||
|
||||
let timezoneUtcOffsetFromBrowser = typeof req.header === 'function' && -Number(req.header('x-user-timezoneoffset'));
|
||||
@@ -386,16 +386,16 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
if (timezoneUtcOffsetFromBrowser !== timezoneUtcOffsetFromUserPrefs) {
|
||||
// The user's browser has just told Habitica that the user's timezone has
|
||||
// changed so store and use the new zone.
|
||||
this.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
|
||||
user.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
|
||||
timezoneUtcOffsetFromUserPrefs = timezoneUtcOffsetFromBrowser;
|
||||
}
|
||||
|
||||
let lastCronTime = this.lastCron;
|
||||
if (this.auth.timestamps.loggedIn < lastCronTime) {
|
||||
lastCronTime = this.auth.timestamps.loggedIn;
|
||||
let lastCronTime = user.lastCron;
|
||||
if (user.auth.timestamps.loggedIn < lastCronTime) {
|
||||
lastCronTime = user.auth.timestamps.loggedIn;
|
||||
}
|
||||
// How many days have we missed using the user's current timezone:
|
||||
let daysMissed = daysSince(lastCronTime, defaults({ now }, this.preferences));
|
||||
let daysMissed = daysSince(lastCronTime, defaults({ now }, user.preferences));
|
||||
|
||||
if (timezoneUtcOffsetAtLastCron !== timezoneUtcOffsetFromUserPrefs) {
|
||||
// Give the user extra time based on the difference in timezones
|
||||
@@ -410,7 +410,7 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
const daysMissedOldZone = daysSince(lastCronTime, defaults({
|
||||
now,
|
||||
timezoneUtcOffsetOverride: timezoneUtcOffsetAtLastCron,
|
||||
}, this.preferences));
|
||||
}, user.preferences));
|
||||
|
||||
if (timezoneUtcOffsetAtLastCron > timezoneUtcOffsetFromUserPrefs) {
|
||||
// The timezone change was in the unsafe direction.
|
||||
@@ -447,12 +447,12 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
const timezoneOffsetDiff = timezoneUtcOffsetFromUserPrefs - timezoneUtcOffsetAtLastCron;
|
||||
// e.g., for dangerous zone change: -300 - -240 = -60 or 600 - 660= -60
|
||||
|
||||
this.lastCron = moment(lastCronTime).subtract(timezoneOffsetDiff, 'minutes');
|
||||
user.lastCron = moment(lastCronTime).subtract(timezoneOffsetDiff, 'minutes');
|
||||
// NB: We don't change this.auth.timestamps.loggedin so that will still record
|
||||
// the time that the previous cron actually ran.
|
||||
// From now on we can ignore the old timezone:
|
||||
// This is still timezoneOffset for backwards compatibility reasons.
|
||||
this.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
|
||||
user.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
|
||||
} else {
|
||||
// Both old and new timezones indicate that cron should
|
||||
// NOT run.
|
||||
@@ -474,6 +474,10 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
return { daysMissed, timezoneUtcOffsetFromUserPrefs };
|
||||
};
|
||||
|
||||
schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
return schema.statics.daysUserHasMissed(this, now, req);
|
||||
};
|
||||
|
||||
async function getUserGroupData (user) {
|
||||
const userGroups = user.getGroups();
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import nconf from 'nconf';
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import mongoose from 'mongoose';
|
||||
import redis from 'redis';
|
||||
import logger from './libs/logger';
|
||||
|
||||
// Setup translations
|
||||
@@ -20,22 +18,12 @@ import './libs/setupFirebase';
|
||||
import './models/challenge';
|
||||
import './models/group';
|
||||
import './models/user';
|
||||
import SERVER_STATUS from './libs/serverStatus';
|
||||
|
||||
connectToMongoDB();
|
||||
|
||||
const server = http.createServer();
|
||||
const app = express();
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('SIGTERM signal received: closing HTTP server');
|
||||
server.close(async () => {
|
||||
await mongoose.disconnect();
|
||||
await redis.quit();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
app.set('port', nconf.get('PORT'));
|
||||
|
||||
attachMiddlewares(app, server);
|
||||
@@ -43,7 +31,6 @@ attachMiddlewares(app, server);
|
||||
server.on('request', app);
|
||||
server.listen(app.get('port'), () => {
|
||||
logger.info(`Express server listening on port ${app.get('port')}`);
|
||||
SERVER_STATUS.EXPRESS = true;
|
||||
});
|
||||
|
||||
export default server;
|
||||
|
||||
Reference in New Issue
Block a user