mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-20 03:14:01 -05:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed48e5e34c | |||
| c17db4ebcd | |||
| 7cc0696ee4 | |||
| 4532105749 | |||
| 0ae19d9107 | |||
| 68bfebcf30 | |||
| 3e93911e70 | |||
| 4ea8636f03 | |||
| 9f97a09b8c | |||
| eccc115b73 | |||
| 2b26eb2bd1 | |||
| 8e042cabc4 | |||
| 8abe167848 | |||
| 3414f962e2 | |||
| 1b68e6d4d3 |
+1
-1
Submodule habitica-images updated: 8a96a0ff62...359153997e
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.44.2",
|
||||
"version": "5.45.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.44.2",
|
||||
"version": "5.45.0",
|
||||
"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.44.2",
|
||||
"version": "5.45.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
// TODO how to test this route since it points to a file on AWS s3?
|
||||
|
||||
describe('GET /export/avatar-:memberId.png', () => {});
|
||||
@@ -1060,6 +1060,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_elven_citadel {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_elven_citadel.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_enchanted_music_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_enchanted_music_room.png');
|
||||
width: 141px;
|
||||
@@ -1931,6 +1936,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_riding_a_comet {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_riding_a_comet.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_rime_ice {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rime_ice.png');
|
||||
width: 141px;
|
||||
@@ -2427,6 +2437,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_waterfall_with_rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_waterfall_with_rainbow.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_wedding_arch {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_wedding_arch.png');
|
||||
width: 141px;
|
||||
@@ -29800,6 +29815,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_handstandOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_handstandOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_hattersSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_hattersSuit.png');
|
||||
width: 114px;
|
||||
@@ -30075,6 +30095,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_softYellowSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softYellowSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_springPetalYukata {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_springPetalYukata.png');
|
||||
width: 114px;
|
||||
@@ -30385,6 +30410,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_floppyYellowHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_floppyYellowHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_flutteryWig {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_flutteryWig.png');
|
||||
width: 114px;
|
||||
@@ -30705,6 +30735,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_verdantArmingCap {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_verdantArmingCap.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_vermilionArcherHelm {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_vermilionArcherHelm.png');
|
||||
width: 90px;
|
||||
@@ -31120,6 +31155,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_softYellowPillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softYellowPillow.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_spanishGuitar {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_spanishGuitar.png');
|
||||
width: 114px;
|
||||
@@ -31170,6 +31210,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_verdantBanner {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_verdantBanner.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_vikingShield {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_vikingShield.png');
|
||||
width: 90px;
|
||||
@@ -31440,6 +31485,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_handstandOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit .png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_hattersSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_hattersSuit.png');
|
||||
width: 114px;
|
||||
@@ -31715,6 +31765,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_softYellowSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softYellowSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_springPetalYukata {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_springPetalYukata.png');
|
||||
width: 114px;
|
||||
@@ -34125,11 +34180,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202605 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202605.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202512.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
|
||||
width: 114px;
|
||||
@@ -34140,11 +34205,31 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202603 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202603.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202605 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202512.png');
|
||||
width: 114px;
|
||||
@@ -34155,6 +34240,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202603 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202603.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_201402 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
|
||||
width: 90px;
|
||||
@@ -36275,6 +36365,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -36595,6 +36705,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -36780,6 +36910,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -37015,6 +37160,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -37255,6 +37420,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_springHealer.png');
|
||||
width: 90px;
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
<p class="purple-600">
|
||||
{{ $t('usernameLimitations') }}
|
||||
</p>
|
||||
<input
|
||||
v-if="needsEmailField"
|
||||
id="emailInput"
|
||||
v-model="email"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('email')"
|
||||
>
|
||||
<div class="custom-control custom-checkbox mb-4">
|
||||
<input
|
||||
id="privacyTOS"
|
||||
@@ -165,6 +173,7 @@ export default {
|
||||
registrationMethod: null,
|
||||
username: '',
|
||||
usernameIssues: [],
|
||||
needsEmailField: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -183,22 +192,30 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
this.authData = this.$store.state.registrationOptions.authData;
|
||||
this.email = this.$store.state.registrationOptions.email;
|
||||
this.username = this.$store.state.registrationOptions.username;
|
||||
this.password = this.$store.state.registrationOptions.password;
|
||||
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
|
||||
|
||||
if (!this.email) {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
if (!this.email) {
|
||||
this.email = window.sessionStorage.getItem('apple-email');
|
||||
}
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
|
||||
if (!this.email && this.registrationMethod !== 'apple') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
|
||||
this.needsEmailField = true;
|
||||
}
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
@@ -237,6 +254,7 @@ export default {
|
||||
idToken: window.sessionStorage.getItem('apple-token'),
|
||||
name: window.sessionStorage.getItem('apple-name'),
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
allowRegister: true,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -498,8 +498,13 @@ export default {
|
||||
|
||||
await this.triggerGetWorldState();
|
||||
this.currentEvent = _find(this.currentEventList, event => Boolean(event.season));
|
||||
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)`;
|
||||
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)`;
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
|
||||
@@ -37,6 +37,7 @@ export default {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
window.sessionStorage.setItem('apple-token', response.idToken);
|
||||
window.sessionStorage.setItem('apple-email', response.email);
|
||||
window.location.href = '/username';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -348,7 +348,6 @@
|
||||
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';
|
||||
@@ -482,25 +481,10 @@ export default {
|
||||
return this.$t('addATask', { type });
|
||||
},
|
||||
badgeCount () {
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
if (this.type === 'reward') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return this.taskList.length;
|
||||
},
|
||||
},
|
||||
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.gearBonus[stat] + statsComputed.classBonus[stat]
|
||||
}}
|
||||
</span>
|
||||
</li>
|
||||
@@ -246,7 +246,9 @@
|
||||
:class="{white: user.preferences.background}"
|
||||
style="overflow:hidden"
|
||||
>
|
||||
<Sprite :image-name="'icon_background_' + user.preferences.background" />
|
||||
<Sprite
|
||||
v-if="user.preferences.background && user.preferences.background !== ''"
|
||||
:image-name="'icon_background_' + user.preferences.background" />
|
||||
</div>
|
||||
<b-popover
|
||||
v-if="label !== 'skip'
|
||||
|
||||
@@ -122,7 +122,7 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
experimentalMinChunkSize: 1000
|
||||
experimentalMinChunkSize: 20000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1063,6 +1063,18 @@
|
||||
"backgroundElegantPalaceText": "Elegant Palace",
|
||||
"backgroundElegantPalaceNotes": "Admire the colorful halls of an Elegant Palace.",
|
||||
|
||||
"backgrounds032026": "SET 142: Released March 2026",
|
||||
"backgroundWaterfallWithRainbowText": "Waterfall with Rainbow",
|
||||
"backgroundWaterfallWithRainbowNotes": "Admire the breathtaking beauty of a Waterfall with a Rainbow.",
|
||||
|
||||
"backgrounds042026": "SET 143: Released April 2026",
|
||||
"backgroundRidingACometText": "Riding a Comet",
|
||||
"backgroundRidingACometNotes": "Travel through space while Riding a Comet!",
|
||||
|
||||
"backgrounds052026": "SET 144: Released May 2026",
|
||||
"backgroundElvenCitadelText": "Elven Citadel",
|
||||
"backgroundElvenCitadelNotes": "Take the scenic journey to an Elven Citadel.",
|
||||
|
||||
"timeTravelBackgrounds": "Steampunk Backgrounds",
|
||||
"backgroundAirshipText": "Airship",
|
||||
"backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.",
|
||||
|
||||
@@ -578,6 +578,15 @@
|
||||
"weaponSpecialWinter2026MageText": "Candelabra Staff",
|
||||
"weaponSpecialWinter2026MageNotes": "Candelabras help by holding multiple candles at a time—follow its lead the next time you need to multitask. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition Winter 2025-2026 Gear.",
|
||||
|
||||
"weaponSpecialSpring2026WarriorText": "Mighty Froggy Foil",
|
||||
"weaponSpecialSpring2026WarriorNotes": "An opportunity to duel might present itself at any moment, and with this formidable foil, you will be ready! Increases Strength by <%= str %>. Limited Edition Spring 2026 Gear.",
|
||||
"weaponSpecialSpring2026RogueText": "Spring Branch",
|
||||
"weaponSpecialSpring2026RogueNotes": "An opportunity to grow is nearly upon you, and with these budding branches, you will be ready! Increases Strength by <%= str %>. Limited Edition Spring 2026 Gear.",
|
||||
"weaponSpecialSpring2026HealerText": "Snowdrop Staff",
|
||||
"weaponSpecialSpring2026HealerNotes": "An opportunity to begin anew with a fresh start is right up ahead, and with this splendid staff, you will be ready! Increases Intelligence by <%= int %>. Limited Edition Spring 2026 Gear.",
|
||||
"weaponSpecialSpring2026MageText": "Maypole Parasol",
|
||||
"weaponSpecialSpring2026MageNotes": "An opportunity to celebrate approaches, and with this pretty parasol pole, you will be ready! Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition Spring 2026 Gear.",
|
||||
|
||||
"weaponMystery201411Text": "Pitchfork of Feasting",
|
||||
"weaponMystery201411Notes": "Stab your enemies or dig in to your favorite foods - this versatile pitchfork does it all! Confers no benefit. November 2014 Subscriber Item.",
|
||||
"weaponMystery201502Text": "Shimmery Winged Staff of Love and Also Truth",
|
||||
@@ -626,6 +635,8 @@
|
||||
"weaponMystery202512Notes": "A shining sword cast from sugar, mint, and arcane enchantments. Confers no benefit. December 2025 Subscriber Item.",
|
||||
"weaponMystery202601Text": "Winter's Aegis",
|
||||
"weaponMystery202601Notes": "An icy bubble shield that grants magical protection from opposing elements. Confers no benefit. January 2026 Subscriber Item.",
|
||||
"weaponMystery202603Text": "Wisteria Wizard Staff",
|
||||
"weaponMystery202603Notes": "Cast spells to warm the spring air and encourage the blossoms to bud! Confers no benefit. March 2026 Subscriber Item.",
|
||||
|
||||
"weaponMystery301404Text": "Steampunk Cane",
|
||||
"weaponMystery301404Notes": "Excellent for taking a turn about town. March 3015 Subscriber Item. Confers no benefit.",
|
||||
@@ -1412,6 +1423,15 @@
|
||||
"armorSpecialWinter2026MageText": "Midwinter Candle Robe",
|
||||
"armorSpecialWinter2026MageNotes": "Glide smoothly along your path like wax on your way to completing your Dailies. Increases Intelligence by <%= int %>. Limited Edition Winter 2025-2026 Gear.",
|
||||
|
||||
"armorSpecialSpring2026WarriorText": "Frog Armor",
|
||||
"armorSpecialSpring2026WarriorNotes": "Spring into action just as soon as the snow begins to thaw. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.",
|
||||
"armorSpecialSpring2026RogueText": "Birch Bark Armor",
|
||||
"armorSpecialSpring2026RogueNotes": "Withstand inevitable spring rains as well as light breezes. Increases Perception by <%= per %>. Limited Edition Spring 2026 Gear.",
|
||||
"armorSpecialSpring2026HealerText": "Snowdrop Gown",
|
||||
"armorSpecialSpring2026HealerNotes": "Glide gracefully from a cold, dark winter into glorious spring. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.",
|
||||
"armorSpecialSpring2026MageText": "Maypole Dancer Outfit",
|
||||
"armorSpecialSpring2026MageNotes": "Arrive ready to dance, picnic, and enjoy the warm weather spring brings. Increases Intelligence by <%= int %>. Limited Edition Spring 2026 Gear.",
|
||||
|
||||
"armorMystery201402Text": "Messenger Robes",
|
||||
"armorMystery201402Notes": "Shimmering and strong, these robes have many pockets to carry letters. Confers no benefit. February 2014 Subscriber Item.",
|
||||
"armorMystery201403Text": "Forest Walker Armor",
|
||||
@@ -1550,6 +1570,8 @@
|
||||
"armorMystery202509Notes": "Bright silks protect you from the weather, hot or cold. Confers no benefit. September 2025 Subscriber Item.",
|
||||
"armorMystery202512Text": "Cookie Champion Armor",
|
||||
"armorMystery202512Notes": "Ready for battle in this plate that is both sweet and strong. Confers no benefit. December 2025 Subscriber Item.",
|
||||
"armorMystery202604Text": "Audacious Astronaut Spacesuit",
|
||||
"armorMystery202604Notes": "One small step for your To Do list, one giant leap for your sense of accomplishment! Confers no benefit. April 2026 Subscriber Item.",
|
||||
|
||||
"armorMystery301404Text": "Steampunk Suit",
|
||||
"armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.",
|
||||
@@ -1800,6 +1822,10 @@
|
||||
"armorArmoireBlackPartyDressNotes": "You’re strong, smart, hearty, and so fashionable! Increases Strength, Intelligence, and Constitution by <%= attrs %> each. Enchanted Armoire: Black Hairbow Set (Item 2 of 2).",
|
||||
"armorArmoireLoneCowpokeOutfitText": "Lone Cowpoke Outfit",
|
||||
"armorArmoireLoneCowpokeOutfitNotes": "Whoa, there! Want to make a statement when you ride into town as a mysterious stranger ready to be productive? Here’s the perfect outfit, complete with chaps and a shining, silver belt buckle. Increases Constitution by <%= con %>. Enchanted Armoire: Lone Cowpoke Set (Item 2 of 2)",
|
||||
"armorArmoireSoftYellowSuitText": "Soft Yellow Suit",
|
||||
"armorArmoireSoftYellowSuitNotes": "Yellow is an energetic color. Wear this to bed, and you will wake up with the sun the next morning ready to tackle a day full of tasks. Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 2 of 3).",
|
||||
"armorArmoireHandstandOutfitText": "Handstand",
|
||||
"armorArmoireHandstandOutfitNotes": "Things sure do look different when you’re upside-down, don’t they? If you’re feeling stuck, it’s time for a fresh perspective! Increases Perception by <%= per %>. Enchanted Armoire: Handstand Set (Item 1 of 1).",
|
||||
|
||||
"headgear": "helm",
|
||||
"headgearCapitalized": "Headgear",
|
||||
@@ -2352,6 +2378,15 @@
|
||||
"headSpecialWinter2026MageText": "Midwinter Candle Hat",
|
||||
"headSpecialWinter2026MageNotes": "Maintain focus and illumination as you set your sights on greater goals this season. Increases Perception by <%= per %>. Limited Edition 2025-2026 Winter Gear.",
|
||||
|
||||
"headSpecialSpring2026WarriorText": "Frog Warrior Helm",
|
||||
"headSpecialSpring2026WarriorNotes": "Frogs are well-known for their resistance to corruption. This helm will grant you their noble qualities! Increases Strength by <%= str %>. Limited Edition Spring 2026 Gear.",
|
||||
"headSpecialSpring2026RogueText": "Spring Branch Helm",
|
||||
"headSpecialSpring2026RogueNotes": "Make a striking statement with twigs and buds growing wild in all directions. Increases Perception by <%= per %>. Limited Edition Spring 2026 Gear.",
|
||||
"headSpecialSpring2026HealerText": "Snowdrop Helm",
|
||||
"headSpecialSpring2026HealerNotes": "Make a hopeful statement with these beautiful, resilient petals. Increases Intelligence by <%= int %>. Limited Edition Spring 2026 Gear.",
|
||||
"headSpecialSpring2026MageText": "Mayflower Crown",
|
||||
"headSpecialSpring2026MageNotes": "Make a joyous statement with bright blooms encircling your head. Increases Perception by <%= per %>. Limited Edition Spring 2026 Gear.",
|
||||
|
||||
"headSpecialGaymerxText": "Rainbow Warrior Helm",
|
||||
"headSpecialGaymerxNotes": "In celebration of the GaymerX Conference, this special helmet is decorated with a radiant, colorful rainbow pattern! GaymerX is a game convention celebrating LGTBQ and gaming and is open to everyone.",
|
||||
|
||||
@@ -2538,7 +2573,11 @@
|
||||
"headMystery202512Text": "Cookie Champion Helm",
|
||||
"headMystery202512Notes": "Gingerbread forged with ancient magic will protect you as long as you can hold off your urge to try a bite! Confers no benefit. December 2025 Subscriber Item.",
|
||||
"headMystery202602Text": "Sakura Fox Ears",
|
||||
"headMystery202602Notes": " Your hearing will be sharpened by these ears such that you can hear the buds of blossoms growing on tree branches as spring approaches. Confers no benefit. February 2026 Subscriber Item.",
|
||||
"headMystery202602Notes": "Your hearing will be sharpened by these ears such that you can hear the buds of blossoms growing on tree branches as spring approaches. Confers no benefit. February 2026 Subscriber Item.",
|
||||
"headMystery202603Text": "Wisteria Wizard Hat",
|
||||
"headMystery202603Notes": "This jaunty hat not only enhances your magical ability, it also has a lovely spring scent! Confers no benefit. March 2026 Subscriber Item.",
|
||||
"headMystery202604Text": "Audacious Astronaut Helmet",
|
||||
"headMystery202604Notes": "In space, no one can hear you check off your To Do’s. But the real reward is your sense of personal accomplishment! Confers no benefit. April 2026 Subscriber Item.",
|
||||
|
||||
"headMystery301404Text": "Fancy Top Hat",
|
||||
"headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.",
|
||||
@@ -2769,6 +2808,10 @@
|
||||
"headArmoireBlacksmithsGogglesNotes": "Shatter and heat-resistant ocular protection is yours when you’re working in a forge. Increases Perception by <%= per %>. Enchanted Armoire: Blacksmith Set (Item 1 of 3).",
|
||||
"headArmoireLoneCowpokeHatText": "Lone Cowpoke Hat",
|
||||
"headArmoireLoneCowpokeHatNotes": "Howdy there, pardner! D’you hate when you’re out on the range, workin’ on tasks, and sun gets in your eyes? Well, good thing you’ve got a hat for that now. Increases Perception by <%= per %>. Enchanted Armoire: Lone Cowpoke Set (Item 1 of 2)",
|
||||
"headArmoireFloppyYellowHatText": "Yellow Floppy Hat",
|
||||
"headArmoireFloppyYellowHatNotes": "Many spells have been sewn into this simple hat, giving it a youthful yellow color. Increases all stats by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 1 of 3).",
|
||||
"headArmoireVerdantArmingCapText": "Verdant Page Arming Cap",
|
||||
"headArmoireVerdantArmingCapNotes": "This comfy, cushioned coif makes you battle-ready and helps you withstand anything heavy that could come your way. Increases Perception and Constitution by <%= attrs %> each. Enchanted Armoire: Verdant Page Set (Item 1 of 2).",
|
||||
|
||||
"offhand": "off-hand item",
|
||||
"offHandCapitalized": "Off-Hand Item",
|
||||
@@ -3086,6 +3129,13 @@
|
||||
"shieldSpecialWinter2026HealerText": "Starburst",
|
||||
"shieldSpecialWinter2026HealerNotes": "Stars help with wayfinding, energy, and illumination—all things that help you better conquer a task list. Increases Constitution by <%= con %>. Limited Edition Winter 2025-2026 Gear.",
|
||||
|
||||
"shieldSpecialSpring2026WarriorText": "Frog Warrior Candelabra",
|
||||
"shieldSpecialSpring2026WarriorNotes": "Not only can this candelabra light your way, you can use it to melt any lingering snow and ice. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.",
|
||||
"shieldSpecialSpring2026RogueText": "Spring Branch",
|
||||
"shieldSpecialSpring2026RogueNotes": "Reach out and reach high with these branches. They double as a back scratcher in a pinch. Increases Strength by <%= str %>. Limited Edition Spring 2026 Gear.",
|
||||
"shieldSpecialSpring2026HealerText": "Snowdrop Leaf",
|
||||
"shieldSpecialSpring2026HealerNotes": "Create a light breeze with this fan as the days grow warmer. It doubles as a writing utensil in a pinch. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.",
|
||||
|
||||
"shieldMystery201601Text": "Resolution Slayer",
|
||||
"shieldMystery201601Notes": "This blade can be used to parry away all distractions. Confers no benefit. January 2016 Subscriber Item.",
|
||||
"shieldMystery201701Text": "Time-Freezer Shield",
|
||||
@@ -3116,6 +3166,8 @@
|
||||
"shieldMystery202508Notes": "If you thought one spinning blade was cool looking, try two! Confers no benefit. August 2025 Subscriber Item.",
|
||||
"shieldMystery202511Text": "Frost Shield",
|
||||
"shieldMystery202511Notes": "This rugged shield of icy rock protects you from bad Habits but won't freeze your hands. Confers no benefit. November 2025 Subscriber Item.",
|
||||
"shieldMystery202605Text": "Nightfall Shield",
|
||||
"shieldMystery202605Notes": "Let the moon’s shining light protect you from dangers in the dark. Confers no benefit. May 2026 Subscriber Item.",
|
||||
|
||||
"shieldMystery301405Text": "Clock Shield",
|
||||
"shieldMystery301405Notes": "Time is on your side with this towering clock shield! Confers no benefit. June 3015 Subscriber Item.",
|
||||
@@ -3298,6 +3350,10 @@
|
||||
"shieldArmoireDoubleBassNotes": "Bom doo bom brrrr brr brr brrrr! Gather your party for some grounding or dancing as you listen to music on this deep double bass. Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Musical Instrument Set 2 (Item 3 of 3)",
|
||||
"shieldArmoirePrettyPinkGiftBoxText": "Pretty Pink Present",
|
||||
"shieldArmoirePrettyPinkGiftBoxNotes": "Is this gift from a dear friend? A caring relative? A true love? A secret admirer? Whoever sent it knows you’ll be pleased with what’s inside. Increases all stats by <%= attrs %> each. Enchanted Armoire: Pretty in Pink Set (Item 2 of 2)",
|
||||
"shieldArmoireSoftYellowPillowText": "Soft Yellow Pillow",
|
||||
"shieldArmoireSoftYellowPillowNotes": "The experienced warrior packs a pillow for any expedition. Grow and shine as you consolidate all you’ve learned during past adventures… even while you nap. Increases Intelligence and Perception by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 3 of 3).",
|
||||
"shieldArmoireVerdantBannerText": "Verdant Page Banner",
|
||||
"shieldArmoireVerdantBannerNotes": "Wave your banner high to signal friends it’s time to rally together! Intelligence by <%= int %>. Enchanted Armoire: Verdant Page Set (Item 2 of 2).",
|
||||
|
||||
"back": "Back Accessory",
|
||||
"backBase0Text": "No Back Accessory",
|
||||
@@ -3392,6 +3448,8 @@
|
||||
"backMystery202601Notes": "This mark grants the user control over the elements of the season of cold and frost. Confers no benefit. January 2026 Subscriber Item.",
|
||||
"backMystery202602Text": "Five Tails of Sakura",
|
||||
"backMystery202602Notes": "These fluffy tails are the color of cherry blossoms, a reminder that spring is on the way! Confers no benefit. February 2026 Subscriber Item.",
|
||||
"backMystery202605Text": "Nightfall Nimbus",
|
||||
"backMystery202605Notes": "A glowing aureole of moonlight and starlight to illuminate the darkest night. Confers no benefit. May 2026 Subscriber Item.",
|
||||
|
||||
"backArmoireHarpsichordText": "Harpsichord",
|
||||
"backArmoireHarpsichordNotes": "Pting! Ptiiing! Gather your party for a dinner or picnic and listen to a tinny melody on this harpsichord. Increases Perception and Intelligence by <%= attrs %> each. Enchanted Armoire: Musical Instrument Set 2 (Item 1 of 3)",
|
||||
|
||||
@@ -223,26 +223,30 @@
|
||||
"fall2024UnderworldSorcerorMageSet": "Underworld Sorceror Set (Mage)",
|
||||
"fall2024SpaceInvaderHealerSet": "Space Invader Set (Healer)",
|
||||
"fall2024BlackCatRogueSet": "Black Cat Set (Rogue)",
|
||||
"winter2025MooseWarriorSet": "Moose Warrior Set",
|
||||
"winter2025AuroraMageSet": "Aurora Mage Set",
|
||||
"winter2025StringLightsHealerSet": "String Lights Healer Set",
|
||||
"winter2025SnowRogueSet": "Snow Rogue Set",
|
||||
"spring2025SunshineWarriorSet": "Sunshine Warrior Set",
|
||||
"spring2025CrystalPointRogueSet": "Crystal Point Rogue Set",
|
||||
"spring2025PlumeriaHealerSet": "Plumeria Healer Set",
|
||||
"spring2025MantisMageSet": "Mantis Mage Set",
|
||||
"summer2025ScallopWarriorSet": "Scallop Warrior Set",
|
||||
"summer2025SquidRogueSet": "Squid Rogue Set",
|
||||
"summer2025SeaAngelHealerSet": "Sea Angel Healer Set",
|
||||
"summer2025FairyWrasseMageSet": "Fairy Wrasse Mage Set",
|
||||
"fall2025SasquatchWarriorSet": "Sasquatch Warrior Set",
|
||||
"fall2025SkeletonRogueSet": "Skeleton Rogue Set",
|
||||
"fall2025KoboldHealerSet": "Kobold Healer Set",
|
||||
"fall2025MaskedGhostMageSet": "Masked Ghost Mage Set",
|
||||
"winter2026RimeReaperWarriorSet": "Rime Reaper Warrior Set",
|
||||
"winter2026SkiRogueSet": "Ski Rogue Set",
|
||||
"winter2026PolarBearHealerSet": "Polar Bear Healer Set",
|
||||
"winter2026MidwinterCandleMageSet": "Midwinter Candle Mage Set",
|
||||
"winter2025MooseWarriorSet": "Moose Set (Warrior)",
|
||||
"winter2025AuroraMageSet": "Aurora Set (Mage)",
|
||||
"winter2025StringLightsHealerSet": "String Lights Set (Healer)",
|
||||
"winter2025SnowRogueSet": "Snow Set (Rogue)",
|
||||
"spring2025SunshineWarriorSet": "Sunshine Set (Warrior)",
|
||||
"spring2025CrystalPointRogueSet": "Crystal Point Set (Rogue)",
|
||||
"spring2025PlumeriaHealerSet": "Plumeria Set (Healer)",
|
||||
"spring2025MantisMageSet": "Mantis Set (Mage)",
|
||||
"summer2025ScallopWarriorSet": "Scallop Set (Warrior)",
|
||||
"summer2025SquidRogueSet": "Squid Set (Rogue)",
|
||||
"summer2025SeaAngelHealerSet": "Sea Angel Set (Healer)",
|
||||
"summer2025FairyWrasseMageSet": "Fairy Wrasse Set (Mage)",
|
||||
"fall2025SasquatchWarriorSet": "Sasquatch Set (Warrior)",
|
||||
"fall2025SkeletonRogueSet": "Skeleton Set (Rogue)",
|
||||
"fall2025KoboldHealerSet": "Kobold Set (Healer)",
|
||||
"fall2025MaskedGhostMageSet": "Masked Ghost Set (Mage)",
|
||||
"winter2026RimeReaperWarriorSet": "Rime Reaper Set (Warrior)",
|
||||
"winter2026SkiRogueSet": "Ski Set (Rogue)",
|
||||
"winter2026PolarBearHealerSet": "Polar Bear Set (Healer)",
|
||||
"winter2026MidwinterCandleMageSet": "Midwinter Candle Set (Mage)",
|
||||
"spring2026FrogWarriorSet": "Frog Set (Warrior)",
|
||||
"spring2026BranchRogueSet": "Spring Branch Set (Rogue)",
|
||||
"spring2026SnowdropHealerSet": "Snowdrop Set (Healer)",
|
||||
"spring2026MaypoleMageSet": "Maypole Set (Mage)",
|
||||
"winterPromoGiftHeader": "GIFT A SUBSCRIPTION, GET ONE FREE!",
|
||||
"winterPromoGiftDetails1": "Until January 6th only, when you gift somebody a subscription, you get the same subscription for yourself for free!",
|
||||
"winterPromoGiftDetails2": "Please note that if you or your gift recipient already have a recurring subscription, the gifted subscription will only start after that subscription is cancelled or has expired. Thanks so much for your support! <3",
|
||||
|
||||
@@ -183,6 +183,9 @@
|
||||
"mysterySet202512": "Cookie Champion Set",
|
||||
"mysterySet202601": "Winter's Aegis Set",
|
||||
"mysterySet202602": "Sakura Fox Set",
|
||||
"mysterySet202603": "Wisteria Wizard Set",
|
||||
"mysterySet202604": "Audacious Astronaut Set",
|
||||
"mysterySet202605": "Nightfall Nimbus Set",
|
||||
"mysterySet301404": "Steampunk Standard Set",
|
||||
"mysterySet301405": "Steampunk Accessories Set",
|
||||
"mysterySet301703": "Peacock Steampunk Set",
|
||||
|
||||
@@ -683,6 +683,15 @@ const backgrounds = {
|
||||
backgrounds022026: {
|
||||
elegant_palace: { },
|
||||
},
|
||||
backgrounds032026: {
|
||||
waterfall_with_rainbow: { },
|
||||
},
|
||||
backgrounds042026: {
|
||||
riding_a_comet: { },
|
||||
},
|
||||
backgrounds052026: {
|
||||
elven_citadel: { },
|
||||
},
|
||||
eventBackgrounds: {
|
||||
birthday_bash: {
|
||||
price: 0,
|
||||
|
||||
@@ -29,6 +29,9 @@ export const ARMOIRE_RELEASE_DATES = {
|
||||
musicalInstrumentTwo: { year: 2025, month: 12 },
|
||||
loneCowpoke: { year: 2026, month: 1 },
|
||||
prettyInPink: { year: 2026, month: 2 },
|
||||
yellowLoungewear: { year: 2026, month: 3 },
|
||||
handstand: { year: 2026, month: 4 },
|
||||
verdantPage: { year: 2026, month: 5 },
|
||||
};
|
||||
|
||||
export const EGGS_RELEASE_DATES = {
|
||||
|
||||
@@ -131,6 +131,11 @@ const SEASONAL_SETS = {
|
||||
'spring2025CrystalPointRogueSet',
|
||||
'spring2025PlumeriaHealerSet',
|
||||
'spring2025MantisMageSet',
|
||||
|
||||
'spring2026FrogWarriorSet',
|
||||
'spring2026BranchRogueSet',
|
||||
'spring2026SnowdropHealerSet',
|
||||
'spring2026MaypoleMageSet',
|
||||
],
|
||||
|
||||
summer: [
|
||||
|
||||
@@ -561,6 +561,15 @@ const armor = {
|
||||
con: 10,
|
||||
set: 'loneCowpoke',
|
||||
},
|
||||
softYellowSuit: {
|
||||
con: 9,
|
||||
str: 9,
|
||||
set: 'yellowLoungewear',
|
||||
},
|
||||
handstandOutfit: {
|
||||
per: 10,
|
||||
set: 'handstand',
|
||||
},
|
||||
};
|
||||
|
||||
const back = {
|
||||
@@ -1156,6 +1165,18 @@ const head = {
|
||||
per: 10,
|
||||
set: 'loneCowpoke',
|
||||
},
|
||||
floppyYellowHat: {
|
||||
con: 3,
|
||||
int: 3,
|
||||
per: 3,
|
||||
str: 3,
|
||||
set: 'yellowLoungewear',
|
||||
},
|
||||
verdantArmingCap: {
|
||||
con: 5,
|
||||
per: 5,
|
||||
set: 'verdantPage',
|
||||
},
|
||||
};
|
||||
|
||||
const shield = {
|
||||
@@ -1546,6 +1567,15 @@ const shield = {
|
||||
str: 2,
|
||||
set: 'prettyInPink',
|
||||
},
|
||||
softYellowPillow: {
|
||||
int: 9,
|
||||
per: 9,
|
||||
set: 'yellowLoungewear',
|
||||
},
|
||||
verdantBanner: {
|
||||
int: 10,
|
||||
set: 'verdantPage',
|
||||
},
|
||||
};
|
||||
|
||||
const headAccessory = {
|
||||
|
||||
@@ -72,6 +72,7 @@ const armor = {
|
||||
202504: { },
|
||||
202509: { },
|
||||
202512: { },
|
||||
202604: { },
|
||||
301404: { },
|
||||
301703: { },
|
||||
301704: { },
|
||||
@@ -122,6 +123,7 @@ const back = {
|
||||
202510: { },
|
||||
202601: { },
|
||||
202602: { },
|
||||
202605: { },
|
||||
};
|
||||
|
||||
const body = {
|
||||
@@ -254,6 +256,8 @@ const head = {
|
||||
202507: { },
|
||||
202512: { },
|
||||
202602: { },
|
||||
202603: { },
|
||||
202604: { },
|
||||
301404: { },
|
||||
301405: { },
|
||||
301703: { },
|
||||
@@ -308,6 +312,7 @@ const shield = {
|
||||
202506: { },
|
||||
202508: { },
|
||||
202511: { },
|
||||
202605: { },
|
||||
301405: { },
|
||||
301704: { },
|
||||
};
|
||||
@@ -337,6 +342,7 @@ const weapon = {
|
||||
202511: { },
|
||||
202512: { },
|
||||
202601: { },
|
||||
202603: { },
|
||||
301404: { },
|
||||
};
|
||||
|
||||
|
||||
@@ -839,6 +839,18 @@ const armor = {
|
||||
winter2026Rogue: {
|
||||
set: 'winter2026SkiRogueSet',
|
||||
},
|
||||
spring2026Warrior: {
|
||||
set: 'spring2026FrogWarriorSet',
|
||||
},
|
||||
spring2026Rogue: {
|
||||
set: 'spring2026BranchRogueSet',
|
||||
},
|
||||
spring2026Healer: {
|
||||
set: 'spring2026SnowdropHealerSet',
|
||||
},
|
||||
spring2026Mage: {
|
||||
set: 'spring2026MaypoleMageSet',
|
||||
},
|
||||
};
|
||||
|
||||
const armorStats = {
|
||||
@@ -1988,6 +2000,18 @@ const head = {
|
||||
winter2026Rogue: {
|
||||
set: 'winter2026SkiRogueSet',
|
||||
},
|
||||
spring2026Warrior: {
|
||||
set: 'spring2026FrogWarriorSet',
|
||||
},
|
||||
spring2026Rogue: {
|
||||
set: 'spring2026BranchRogueSet',
|
||||
},
|
||||
spring2026Healer: {
|
||||
set: 'spring2026SnowdropHealerSet',
|
||||
},
|
||||
spring2026Mage: {
|
||||
set: 'spring2026MaypoleMageSet',
|
||||
},
|
||||
};
|
||||
|
||||
const headStats = {
|
||||
@@ -2727,6 +2751,16 @@ const shield = {
|
||||
winter2026Rogue: {
|
||||
set: 'winter2026SkiRogueSet',
|
||||
},
|
||||
spring2026Warrior: {
|
||||
set: 'spring2026FrogWarriorSet',
|
||||
},
|
||||
spring2026Rogue: {
|
||||
set: 'spring2026BranchRogueSet',
|
||||
notes: t('shieldSpecialSpring2026RogueNotes', { str: 8 }),
|
||||
},
|
||||
spring2026Healer: {
|
||||
set: 'spring2026SnowdropHealerSet',
|
||||
},
|
||||
};
|
||||
|
||||
const shieldStats = {
|
||||
@@ -3466,6 +3500,18 @@ const weapon = {
|
||||
winter2026Rogue: {
|
||||
set: 'winter2026SkiRogueSet',
|
||||
},
|
||||
spring2026Warrior: {
|
||||
set: 'spring2026FrogWarriorSet',
|
||||
},
|
||||
spring2026Rogue: {
|
||||
set: 'spring2026BranchRogueSet',
|
||||
},
|
||||
spring2026Healer: {
|
||||
set: 'spring2026SnowdropHealerSet',
|
||||
},
|
||||
spring2026Mage: {
|
||||
set: 'spring2026MaypoleMageSet',
|
||||
},
|
||||
};
|
||||
|
||||
const weaponStats = {
|
||||
|
||||
@@ -1353,8 +1353,6 @@ 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': {
|
||||
@@ -1362,13 +1360,12 @@ api.getLookingForParty = {
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line no-multi-str
|
||||
.select('_id auth.local.username auth.timestamps backer contributor.level \
|
||||
flags.classSelected inbox.blocks invitations.party items.gear.costume \
|
||||
.select('_id auth.blocked auth.local.username auth.timestamps backer contributor.level \
|
||||
flags.chatRevoked 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({ leanUser: true, userFieldsToInclude: ['profile', 'contributor', 'backer', 'inbox'] })],
|
||||
middlewares: [authWithHeaders({ 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({ userFieldsToInclude: ['tags'] })],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -69,7 +69,7 @@ api.createTag = {
|
||||
api.getTags = {
|
||||
method: 'GET',
|
||||
url: '/tags',
|
||||
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['tags'] })],
|
||||
middlewares: [authWithHeaders()],
|
||||
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({ leanUser: true, userFieldsToInclude: ['tags'] })],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -133,7 +133,7 @@ api.getTag = {
|
||||
api.updateTag = {
|
||||
method: 'PUT',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -175,7 +175,7 @@ api.updateTag = {
|
||||
api.reorderTags = {
|
||||
method: 'POST',
|
||||
url: '/reorder-tags',
|
||||
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -215,7 +215,7 @@ api.reorderTags = {
|
||||
api.deleteTag = {
|
||||
method: 'DELETE',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ api.getUserTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/user',
|
||||
middlewares: [authWithHeaders({
|
||||
leanUser: true,
|
||||
// Some fields (including _id, preferences) are always loaded (see middlewares/auth)
|
||||
userFieldsToInclude: ['tasksOrder'],
|
||||
})],
|
||||
async handler (req, res) {
|
||||
@@ -953,7 +953,7 @@ api.addChecklistItem = {
|
||||
api.scoreCheckListItem = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/checklist/:itemId/score',
|
||||
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['_id'] })],
|
||||
middlewares: [authWithHeaders()],
|
||||
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).lean().exec();
|
||||
const tasks = await Tasks.Task.find(query).exec();
|
||||
|
||||
forEach(tasks, task => {
|
||||
task.text = 'task text';
|
||||
|
||||
@@ -22,7 +22,6 @@ api.purchaseHistory = {
|
||||
let transactions = await Transaction
|
||||
.find({ userId: req.params.memberId })
|
||||
.sort({ createdAt: -1 })
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (!res.locals.user.hasPermission('userSupport')) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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,9 +313,7 @@ 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 })
|
||||
.lean();
|
||||
const transactions = await Transaction.find({ userId: user._id }).sort({ createdAt: -1 });
|
||||
res.respond(200, transactions);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,18 +6,10 @@ 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 = {};
|
||||
|
||||
@@ -53,7 +45,7 @@ api.exportUserHistory = {
|
||||
const tasks = await Tasks.Task.find({
|
||||
userId: user._id,
|
||||
type: { $in: ['habit', 'daily'] },
|
||||
}).lean().exec();
|
||||
}).exec();
|
||||
|
||||
const output = [
|
||||
['Task Name', 'Task ID', 'Task Type', 'Date', 'Value'],
|
||||
@@ -92,7 +84,7 @@ async function _getUserDataForExport (user) {
|
||||
const [tasks, messages] = await Promise.all([
|
||||
Tasks.Task.find({
|
||||
userId: user._id,
|
||||
}).lean().exec(),
|
||||
}).exec(),
|
||||
|
||||
inboxLib.getUserInbox(user, { asArray: false }),
|
||||
]);
|
||||
@@ -100,6 +92,7 @@ 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;
|
||||
@@ -156,122 +149,6 @@ 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
|
||||
|
||||
@@ -44,9 +44,13 @@ export async function appleProfile (req) {
|
||||
|
||||
const verifiedPayload = await jwt.verify(idToken, applePublicKey, { algorithms: 'RS256' });
|
||||
|
||||
let { email } = verifiedPayload;
|
||||
if ((!email || email === '') && req.body.email) {
|
||||
email = req.body.email;
|
||||
}
|
||||
return {
|
||||
id: verifiedPayload.sub,
|
||||
emails: [{ value: verifiedPayload.email }],
|
||||
emails: [{ value: email }],
|
||||
name: verifiedPayload.name || req.body.name || req.query.name,
|
||||
idToken,
|
||||
};
|
||||
|
||||
@@ -22,7 +22,6 @@ 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 }, { timestamp: 1 }).lean().exec();
|
||||
const beforeMessage = await Chat.findOne({ _id: before }).exec();
|
||||
if (beforeMessage) {
|
||||
query = query.where('timestamp').lt(beforeMessage.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
const groupChat = await query.limit(effectiveLimit).lean().exec();
|
||||
const groupChat = await query.limit(effectiveLimit).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,
|
||||
}).lean().exec();
|
||||
}).exec();
|
||||
|
||||
for (const usr of loadedUsers) {
|
||||
const loadedUserConversation = {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -65,9 +66,8 @@ if (IS_PROD) {
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (LOGGLY_TOKEN && LOGGLY_SUBDOMAIN) {
|
||||
const tags = ['Winston-NodeJS'];
|
||||
const tags = ['Winston-NodeJS', os.hostname()];
|
||||
if (nconf.get('SERVER_EMOJI')) {
|
||||
tags.push(nconf.get('SERVER_EMOJI'));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getDevelopmentConnectionUrl,
|
||||
getDefaultConnectionOptions,
|
||||
} from './mongodb';
|
||||
import SERVER_STATUS from './serverStatus';
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const MAINTENANCE_MODE = nconf.get('MAINTENANCE_MODE');
|
||||
@@ -24,6 +25,13 @@ 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,10 +169,7 @@ api.subscribe = async function subscribe (user, receipt, headers, nextPaymentPro
|
||||
{ 'purchased.plan.customerId': purchase.originalTransactionId },
|
||||
{ 'purchased.plan.customerId': purchase.transactionId },
|
||||
],
|
||||
}, {
|
||||
_id: 1,
|
||||
'purchased.plan': 1,
|
||||
}).lean().exec();
|
||||
}).exec();
|
||||
if (existingUsers.length > 0) {
|
||||
if (purchase.originalTransactionId === purchase.transactionId) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
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 },
|
||||
).lean().exec();
|
||||
).exec();
|
||||
}
|
||||
if (upgradedGroups.length > 0) {
|
||||
for (const upgradedGroup of upgradedGroups) {
|
||||
@@ -270,6 +270,7 @@ 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;
|
||||
}
|
||||
});
|
||||
@@ -302,17 +303,7 @@ async function getTasks (req, res, options = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
if (ownerDirty) await owner.save();
|
||||
|
||||
// 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;
|
||||
const optionsForShouldDo = user.preferences.toObject();
|
||||
optionsForShouldDo.now = now;
|
||||
task.isDue = shared.shouldDo(dateTaskIsDue, task, optionsForShouldDo);
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ export async function update (req, res, { isV3 = false }) {
|
||||
],
|
||||
}, {
|
||||
_id: 1,
|
||||
}).lean().exec();
|
||||
}).exec();
|
||||
|
||||
matchingGroupsArray = _.map(matchingGroups, groupRecord => groupRecord._id);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
|
||||
@@ -47,6 +48,14 @@ 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');
|
||||
});
|
||||
@@ -56,6 +65,8 @@ 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.on('close', () => {
|
||||
req.once('close', () => {
|
||||
const requestTime = Date.now() - req.requestStartTime;
|
||||
if (requestTime > SLOW_REQUEST_THRESHOLD) {
|
||||
const data = buildBaseLogData(req);
|
||||
|
||||
@@ -43,7 +43,6 @@ 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,5 +1,6 @@
|
||||
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
|
||||
@@ -18,3 +19,19 @@ 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.statics.daysUserHasMissed = function daysUserHasMissed (user, now, req = {}) {
|
||||
schema.methods.daysUserHasMissed = function daysUserHasMissed (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 = common.fns.getUtcOffset(user);
|
||||
const timezoneUtcOffsetAtLastCron = Number.isFinite(user.preferences.timezoneOffsetAtLastCron)
|
||||
? -user.preferences.timezoneOffsetAtLastCron
|
||||
let timezoneUtcOffsetFromUserPrefs = this.getUtcOffset();
|
||||
const timezoneUtcOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron)
|
||||
? -this.preferences.timezoneOffsetAtLastCron
|
||||
: timezoneUtcOffsetFromUserPrefs;
|
||||
|
||||
let timezoneUtcOffsetFromBrowser = typeof req.header === 'function' && -Number(req.header('x-user-timezoneoffset'));
|
||||
@@ -386,16 +386,16 @@ schema.statics.daysUserHasMissed = function daysUserHasMissed (user, 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.
|
||||
user.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
|
||||
this.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
|
||||
timezoneUtcOffsetFromUserPrefs = timezoneUtcOffsetFromBrowser;
|
||||
}
|
||||
|
||||
let lastCronTime = user.lastCron;
|
||||
if (user.auth.timestamps.loggedIn < lastCronTime) {
|
||||
lastCronTime = user.auth.timestamps.loggedIn;
|
||||
let lastCronTime = this.lastCron;
|
||||
if (this.auth.timestamps.loggedIn < lastCronTime) {
|
||||
lastCronTime = this.auth.timestamps.loggedIn;
|
||||
}
|
||||
// How many days have we missed using the user's current timezone:
|
||||
let daysMissed = daysSince(lastCronTime, defaults({ now }, user.preferences));
|
||||
let daysMissed = daysSince(lastCronTime, defaults({ now }, this.preferences));
|
||||
|
||||
if (timezoneUtcOffsetAtLastCron !== timezoneUtcOffsetFromUserPrefs) {
|
||||
// Give the user extra time based on the difference in timezones
|
||||
@@ -410,7 +410,7 @@ schema.statics.daysUserHasMissed = function daysUserHasMissed (user, now, req =
|
||||
const daysMissedOldZone = daysSince(lastCronTime, defaults({
|
||||
now,
|
||||
timezoneUtcOffsetOverride: timezoneUtcOffsetAtLastCron,
|
||||
}, user.preferences));
|
||||
}, this.preferences));
|
||||
|
||||
if (timezoneUtcOffsetAtLastCron > timezoneUtcOffsetFromUserPrefs) {
|
||||
// The timezone change was in the unsafe direction.
|
||||
@@ -447,12 +447,12 @@ schema.statics.daysUserHasMissed = function daysUserHasMissed (user, now, req =
|
||||
const timezoneOffsetDiff = timezoneUtcOffsetFromUserPrefs - timezoneUtcOffsetAtLastCron;
|
||||
// e.g., for dangerous zone change: -300 - -240 = -60 or 600 - 660= -60
|
||||
|
||||
user.lastCron = moment(lastCronTime).subtract(timezoneOffsetDiff, 'minutes');
|
||||
this.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.
|
||||
user.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
|
||||
this.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
|
||||
} else {
|
||||
// Both old and new timezones indicate that cron should
|
||||
// NOT run.
|
||||
@@ -474,10 +474,6 @@ schema.statics.daysUserHasMissed = function daysUserHasMissed (user, 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,6 +1,8 @@
|
||||
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
|
||||
@@ -18,12 +20,22 @@ 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);
|
||||
@@ -31,6 +43,7 @@ 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