Compare commits

...

174 Commits

Author SHA1 Message Date
SabreCat
5a13231027 4.230.2 2022-05-17 09:33:34 -05:00
Natalie L
4e78d72cee May 2022 Magic Hatching Potions and Quest Bundle (#13979)
* May 2022 magic hatching potions and quest bundle

* fix(string): typo

* fix(content): add and use event

Co-authored-by: SabreCat <sabe@habitica.com>
2022-05-17 09:32:50 -05:00
SabreCat
aa2458d564 4.230.1 2022-05-12 13:55:59 -05:00
Weblate
a1a3022392 Translated using Weblate (Japanese)
Currently translated at 99.6% (2585 of 2593 strings)

Translated using Weblate (Spanish)

Currently translated at 98.6% (2558 of 2593 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (690 of 690 strings)

Translated using Weblate (Lithuanian)

Currently translated at 6.8% (9 of 131 strings)

Translated using Weblate (Filipino)

Currently translated at 64.8% (1681 of 2593 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.9% (2048 of 2593 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2593 of 2593 strings)

Translated using Weblate (Spanish)

Currently translated at 98.6% (2558 of 2593 strings)

Translated using Weblate (Ukrainian)

Currently translated at 93.3% (168 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 90.1% (193 of 214 strings)

Translated using Weblate (Filipino)

Currently translated at 65.9% (1709 of 2593 strings)

Translated using Weblate (Korean)

Currently translated at 31.6% (57 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Spanish)

Currently translated at 98.6% (2558 of 2593 strings)

Co-authored-by: Elena Balionyte <ziuze15spam@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Justin Cho <jcho93@umd.edu>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Vince Vilan <vincemorel.vilan.889@my.csun.edu>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/lt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Limited
2022-05-12 20:48:18 +02:00
dependabot[bot]
91fdeb0e51 build(deps): bump jwks-rsa from 2.1.0 to 2.1.1 (#13972)
Bumps [jwks-rsa](https://github.com/auth0/node-jwks-rsa) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/auth0/node-jwks-rsa/releases)
- [Changelog](https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jwks-rsa/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: jwks-rsa
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-11 12:13:36 -04:00
dependabot[bot]
3a8fbbe394 build(deps): bump stripe from 8.219.0 to 8.222.0 (#13970)
Bumps [stripe](https://github.com/stripe/stripe-node) from 8.219.0 to 8.222.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v8.219.0...v8.222.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-11 12:13:20 -04:00
SabreCat
bc1708be14 Merge branch 'release' into develop 2022-05-10 13:57:33 -05:00
SabreCat
0a22038d05 4.230.0 2022-05-10 13:56:00 -05:00
Weblate
75ce44e6d9 Translated using Weblate (Filipino)
Currently translated at 66.1% (1715 of 2593 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (2583 of 2587 strings)

Translated using Weblate (Spanish)

Currently translated at 98.8% (2558 of 2587 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Filipino)

Currently translated at 66.1% (1711 of 2587 strings)

Translated using Weblate (Spanish)

Currently translated at 98.8% (2558 of 2587 strings)

Translated using Weblate (Filipino)

Currently translated at 96.1% (126 of 131 strings)

Translated using Weblate (German)

Currently translated at 100.0% (198 of 198 strings)

Translated using Weblate (German)

Currently translated at 99.9% (2585 of 2587 strings)

Translated using Weblate (Filipino)

Currently translated at 90.8% (119 of 131 strings)

Translated using Weblate (Spanish)

Currently translated at 98.8% (2558 of 2587 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (677 of 690 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 66.0% (1708 of 2587 strings)

Translated using Weblate (Spanish)

Currently translated at 98.8% (2558 of 2587 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Filipino)

Currently translated at 66.0% (1708 of 2587 strings)

Translated using Weblate (Filipino)

Currently translated at 65.8% (1704 of 2587 strings)

Translated using Weblate (German)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (German)

Currently translated at 100.0% (690 of 690 strings)

Co-authored-by: Anton Kapustinsky <anton.kap02@mail.ru>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Mara S. (Dolichotis) <marascherzer@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Vince Vilan <vincemorel.vilan.889@my.csun.edu>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/de/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Contrib
Translation: Habitica/Gear
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
2022-05-10 14:02:46 +02:00
SabreCat
8e1bc6bcd7 chore(sprites): compile
also fix a few string typos
2022-05-09 14:23:51 -05:00
CuriousMagpie
beb51fb00d 2022-05 backgrounds and enchanted 2022-05-09 14:23:45 -05:00
CuriousMagpie
7548834442 2022-05 backgrounds and enchanted armoire images 2022-05-09 14:23:37 -05:00
Natalie L
d317bd1c41 May 2022 Backgrounds and Enchanted Armoire (#13966)
* 2022-05 backgrounds and enchanted armoire images

* 2022-05 backgrounds and enchanted

* chore(sprites): compile
also fix a few string typos

Co-authored-by: SabreCat <sabe@habitica.com>
2022-05-09 14:21:25 -05:00
Alys
1a920d6e17 Hall of Heroes / Contributors: fix hasPermissions bug, merge Name and User ID columns (#13968)
* fix bug in hasPermissions call to stop normal users seeing UserID column

* merge User ID column into Name column in Hall of Heroes
2022-05-09 13:55:52 -05:00
Alys
2a4886b325 merge User ID column into Name column in Hall of Heroes 2022-05-09 13:53:51 -05:00
Alys
a1d0403782 fix bug in hasPermissions call to stop normal users seeing UserID column 2022-05-09 13:53:40 -05:00
Sabe Jones
9de6f7b3bc fix(Docker): include failsafe for Git HTTPS 2022-05-06 17:26:48 -05:00
Natalie L
5e12b7b042 Fix: Inconsistent Quest Unlock Behavior (#13734)
* remove quest refactoring, created new branch for that task

* remove quest refactoring, created new branch for that task

* More trying to figure out how buying a quest actually works

* rolling back changes

* updated shops/quests/index.vue to disable clicking on locked quests

* removed console.log(item)

* misc fixes per review comments

* changes as requested

* incorporated quest refactors updates

* removing a couple lines of code
2022-05-05 16:51:47 -05:00
Skander KRATOU
de4ebbac7b Habit streak Fix (#13947)
* Fixes Issue13749

* If a value is at 0 the other value won't show a +/-

* Fix for the negative habits

* Fix : Habits values will always have signs except if the habit is one sided or the values are null

* fix(tasks): tighten margen when 0 as well

Co-authored-by: SabreCat <sabe@habitica.com>
2022-05-05 16:50:50 -05:00
dependabot[bot]
ea3b27ff17 build(deps): bump @babel/preset-env from 7.16.11 to 7.17.10 (#13960)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.11 to 7.17.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.10/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 15:54:30 -04:00
dependabot[bot]
8c3c4c1d49 build(deps): bump express from 4.17.3 to 4.18.1 (#13961)
Bumps [express](https://github.com/expressjs/express) from 4.17.3 to 4.18.1.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.3...4.18.1)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 15:53:59 -04:00
dependabot[bot]
50577fa39b build(deps): bump remove-markdown from 0.3.0 to 0.5.0 (#13962)
Bumps [remove-markdown](https://github.com/stiang/remove-markdown) from 0.3.0 to 0.5.0.
- [Release notes](https://github.com/stiang/remove-markdown/releases)
- [Commits](https://github.com/stiang/remove-markdown/commits)

---
updated-dependencies:
- dependency-name: remove-markdown
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 15:53:21 -04:00
dependabot[bot]
30c4dd86df build(deps-dev): bump axios from 0.26.1 to 0.27.2 (#13959)
Bumps [axios](https://github.com/axios/axios) from 0.26.1 to 0.27.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.26.1...v0.27.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 15:52:56 -04:00
Weblate
f9857efbac Merge branch 'origin/develop' into Weblate. 2022-05-05 20:57:21 +02:00
Weblate
1a7b264958 Translated using Weblate (French)
Currently translated at 100.0% (198 of 198 strings)

Translated using Weblate (French)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (198 of 198 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.9% (2507 of 2587 strings)

Translated using Weblate (Spanish)

Currently translated at 98.8% (2558 of 2587 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.5% (2499 of 2587 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.5% (2499 of 2587 strings)

Translated using Weblate (Filipino)

Currently translated at 65.8% (1703 of 2587 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 97.7% (738 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (198 of 198 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (198 of 198 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (2537 of 2587 strings)

Translated using Weblate (Filipino)

Currently translated at 65.6% (1698 of 2587 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 97.0% (733 of 755 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 95.3% (2466 of 2587 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 95.3% (2466 of 2587 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 92.9% (118 of 127 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 96.6% (205 of 212 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (2583 of 2587 strings)

Translated using Weblate (Spanish)

Currently translated at 98.8% (2558 of 2587 strings)

Translated using Weblate (Filipino)

Currently translated at 65.4% (1694 of 2587 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 94.8% (2454 of 2587 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Filipino)

Currently translated at 87.3% (97 of 111 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 94.7% (2450 of 2587 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2584 of 2587 strings)

Translated using Weblate (German)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2587 of 2587 strings)

Translated using Weblate (French)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 82.6% (105 of 127 strings)

Translated using Weblate (Filipino)

Currently translated at 76.5% (85 of 111 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (365 of 365 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2587 of 2587 strings)

Translated using Weblate (Filipino)

Currently translated at 69.3% (77 of 111 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 94.4% (2440 of 2583 strings)

Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Erick Diego Castillo Chavez <erick.diego.c.c@gmail.com>
Co-authored-by: Heitor Menezes Gomes <heitorgmenezes@gmail.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: JoanZeppeli <x17501668978@163.com>
Co-authored-by: José Wellington Carneiro <wellingtonjco@gmail.com>
Co-authored-by: Kedr <sergeysamori.ua@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Vince Vilan <vincemorel.vilan.889@my.csun.edu>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2022-05-05 20:57:10 +02:00
SabreCat
d6bd10fa25 4.229.2 2022-05-05 13:56:08 -05:00
SabreCat
55f3792c96 Merge branch 'develop' into release 2022-05-05 13:56:02 -05:00
dependabot[bot]
a390dd82a7 build(deps): bump superagent from 7.1.2 to 7.1.3 (#13963)
Bumps [superagent](https://github.com/visionmedia/superagent) from 7.1.2 to 7.1.3.
- [Release notes](https://github.com/visionmedia/superagent/releases)
- [Changelog](https://github.com/visionmedia/superagent/blob/master/HISTORY.md)
- [Commits](https://github.com/visionmedia/superagent/commits/v7.1.3)

---
updated-dependencies:
- dependency-name: superagent
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-04 17:10:52 -04:00
dependabot[bot]
d47f30fc10 build(deps): bump jwks-rsa from 2.0.5 to 2.1.0 (#13958)
Bumps [jwks-rsa](https://github.com/auth0/node-jwks-rsa) from 2.0.5 to 2.1.0.
- [Release notes](https://github.com/auth0/node-jwks-rsa/releases)
- [Changelog](https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jwks-rsa/compare/v2.0.5...v2.1.0)

---
updated-dependencies:
- dependency-name: jwks-rsa
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-04 17:09:20 -04:00
dependabot[bot]
03744c63f6 build(deps): bump rate-limiter-flexible from 2.3.6 to 2.3.7 (#13956)
Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 2.3.6 to 2.3.7.
- [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases)
- [Commits](https://github.com/animir/node-rate-limiter-flexible/commits)

---
updated-dependencies:
- dependency-name: rate-limiter-flexible
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-04 17:09:04 -04:00
dependabot[bot]
f2a418e3fa build(deps): bump @babel/core from 7.17.9 to 7.17.10 (#13955)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.9 to 7.17.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.10/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-04 17:08:44 -04:00
Sabe Jones
1477326351 Log analytics event when cron fails (#13945)
* feat(debug): log analytics event when cron fails

* feat(debug): include status code

Co-authored-by: SabreCat <sabe@habitica.com>
2022-05-03 14:42:45 -05:00
Phillip Thelen
38b39b600c Adminpanel and revamped permissions (#13843)
* create Admin Panel page with initial content from Hall's admin section

* reorganise Admin Panel form and add more accordians

* add lastCron to fields returned by api.getHeroes

* improve timestamps and authentication section

* add party and quest info to Admin Panel, add party to heroAdminFields

* move Admin Panel menu item to top of menu, make invisible to non-admins

* remove code used for displaying all Heroes

* add avatar appearance and drops section in Admin Panel

* allow logged-in user to be the default hero loaded

* add time zones to timestamp/authentication section

* rename Items to Update Items

This will allow a new Items section to be added.

* add read-only Items display with button to copy data to Update Items section

* remove never-used allItemsPaths code that had been copied from Hall

* update tests for the attributes added to heroAdminFields

* supply names for items and also set information for gear/equipment

* remove code that loads subsections of content

We use enough of the content that it's easier to load it all and
access it through the content object, especially when we're looping
through different item types.

* add gear names and set details to Avatar Costume/Battle Gear section

* make the wiki URLs clickable and make minor item format improvements

* add gear sets for Check-In Incentives and animal ears and tails

* add gear set for Gold-Purchasable Quest Lines

Also merges the existing Mystery of the Masterclassers quest set into it.

* fix error with Kickstarter gear set and include wiki link

* improve description of check-in incentive gear set

* fix description of Items section

* fix lint warnings

* update another test for the attributes added to heroAdminFields

* allow "@" to be included when specifying Username to load

* create GetHeroParty API v3 route to fetch a given user's party data

Only some data from the party will be loaded (e.g., not private
data such as name, description).

Includes tests for the route.

See the next commit for front-end changes that use this.

* display data from a given user's party in admin panel

Only some data from the party will be loaded (e.g., not private
data such as name, description).

Also adds support for finding and displaying errors from the
user's data.

* use new error handling method for other sections

- Time zone differences
- Cron bugs
- Privilege removal (mute/block) - not a bug but needs to be highlighted

* redirect non-admin users away from admin-only page (WIP)

This needs more work. Currently, admin users are also redirected
if they access the page by direct URL or after reload.

* clarify source of items from Check-In Incentives and Lunar Battle quests

* replace non-standard form fields with HTML forms

* add user's language, remove unused export blocks

* convert functions to filters: formatDate, formatTimeZone

* improve display of minutes portion of time zone in Admin Panel

* move basic details about user to a new component

* move Timestamp/Cron/Auth/etc details to a new component - WIP, has errors

The automatic expand and error warnings don't reset themselves when
you fetch data for a new user.

* replace non-standard form fields with HTML forms

Most of this was done in 26fdcbbee5

* move Timestamp/Cron/Auth/etc details to a new component (fixed)

* move Avatar and Drops section to a new component

* move Party and Quest section to a new component

* move Contributor Details to new component, add checkbox for admin, add preview

This adds a markdown-enabled preview of the Contributions textarea.

It also removes the code that automatically set contributor.admin
to true when the Tier was above 7.
That feature wasn't secure because the Tier can be accidentally
changed if you scroll while the cursor is over the Tier form field
(we accidentally demoted a Socialite once by doing that and if
we'd scrolled in the other direction we would have given her
admin privileges).

Instead there's now a checkbox for giving moderator-level privileges.
We'll want that anyway when we move to a system of selected
privileges for each admin instead of all admin privileges being
given to all mods/staff.

There's also a commented-out checkbox for giving Bailey CMS
privileges, for when we're ready to use that. The User model doesn't
yet have support for it.

* move Privileges and Gems section to a new component

* rename formatItems to getItemDescription; make other minor fixes

* remove an outdated test description

This "pended" explanation probably wasn't needed after "x" was
removed from "describe" in 2ab76db27c

* add newsPoster Bailey CMS permission to User model and Admin Panel

* move formatDate from mixins to filters

* make lint fixes

* remove development comments from hall.js

I'll be handling the TODO comment and I've left in my "XXX" marker
to remind me

* fix bug in Hall's castItemVal: mounts are null not false

* move Items section to a new component and delete Update Items section

The Update Items section is no longer needed because the new Items
component has in-place editing.

* remove unused imports

* add "secret" field to "Privileges, Gem Balance" section.

Also move the markdownPreview style from contributorDetails.vue to
index.vue since it's used in two components now.

* show non-Standard never-owned Pets and Mounts in Items section

* redirect non-admin users away from admin-only page

This completes the work started in commit a4f9c754ad

It now allows admins to access the page when coming from another
page on the site or from a direct link, including if the admin user
isn't logged in yet.

* display memberCount for party

* add secret.text field to Contributor Details

This is in addition to showing it in the Privileges section because
the secret text could be about either troublesome behaviour or
contributions.

* allow user to be loaded into Admin Panel via a URL

This includes:

- router config has a child route for the admin panel with a
Username/ID as a parameter
- loadHero code moved from top-level index page into a new
"user support" index page
- links in the Hall changed to point to admin panel route
- admin panel link added to admin section of user profile modal

* keep list of known titles on their own lines

* sort heroFields alphabetically

No actual changes.

* return all flags for use in Admin Panel and fix Hall tests for flags

Future Admin Panel changes will display more flags.

NB 'flags' wasn't in the tests before, even though two optional
flags were being fetched.
The tests weren't failing because the test users hadn't been given
data for those optional flags.

The primary reason for this change now is to fix the tests.

* show part of the API Token in the Admin Panel

* send full hero object into cronAndAuth.vue

This is a prelude to allowing this component to change the hero.

* split heroAdminFields string into two: one for fetching data and one for showing it

This is because apiToken must be fetched but not shown,
while apiTokenObscured is calculated (not fetched) and shown.

* let admin change a user's API Token

* restore sanity

* remove code to show obscured version of API Token

It will return with tighter permissions for viewing it.

* add Custom Day Start time (CDS) to Timestamps, Time Zone... section

* commit lint's automatic fixes - one for admin-panel changes in hall.js

The other fixes aren't related to this PR but I figured they may
as well go live.

* apply fixes from paglias's comments, excluding style/CSS changesd

The comments that this PR fixes start at
https://github.com/HabitRPG/habitica/pull/12035#pullrequestreview-500422316

Style fixes will be in a future commit.

* fix styles/CSS

* allow profile modal to close when using admin panel link

Also removes an empty components block.

* prevent Admin Panel being used without new userSupport privilege

Also adds initial support for other contributor.priv privileges
and changes Debug Menu to add userSupport privilege

* don't do this: this.hero = { ...hero };

* enhance quest error messages

* redirect to admin-panel home page when using "Save and Clear Data"

The user's ID / name is still in the form for easy refetching.

* create ensurePriv function, use in api.getHeroParty

* fix lint problems and integration tests

* add page title to top-level Admin Panel

Also add more details to a router comment (consistent with a similar
comment) in case it helps anyone.

* fix tests

* display Moderation Notes above Contributions

* lint fix

* remove placeholder code for new privileges

I had planned to have each of these implemented in stages, but
paglias wanted it all done at once. I'm afraid that's too big a
project for me to take on in a single PR so I'm cancelling
the plans for adjusting the privileges.

* Improve permission handling

* Don't report timezone error on first day

* fix lint error

* .

* Fix lint error

* fix failing tests

* Fix more tests

* .

* ..

* ...

* fix(admin): always include permissions when querying user
also remove unnecessary failing test case

* permission improvements

* show transactions in admin panel

* fix lint errors

* fix permission check

* fix(panel): missing mixin, handle empty perms object

Co-authored-by: Alys <alice.harris@oldgods.net>
Co-authored-by: SabreCat <sabe@habitica.com>
2022-05-03 14:40:56 -05:00
SabreCat
864293b62b 4.229.1 2022-05-02 14:26:47 -05:00
SabreCat
bada094139 chore(login): remove FB login option on web client 2022-05-02 14:26:42 -05:00
Aleksandr Saitgalin
1823f658c6 update user and group in a transaction when creating a group. fixes #12124 (#13730)
* fix #12124

add a transaction for updating user and group
so the user doesn't lose gems when saving the group fails

* use mongoose transaction helper

use the helper instead of manually commiting/aborting
to deal with transient transaction errors

* increase timeout and add console.log outputs

add some logging to a failing test

* Revert "increase timeout and add console.log outputs"

This reverts commit 0c36aaa55f.

* add a test for gems when guild creation fails

test the transaction in createGroup()
make sure user keeps the gems if group.save() rejects

* fix(test): try suggested delay from PR discussion

Co-authored-by: SabreCat <sabe@habitica.com>
2022-04-29 16:47:17 -05:00
dependabot[bot]
9a3e3c93eb build(deps): bump glob from 7.2.0 to 8.0.1 (#13938)
Bumps [glob](https://github.com/isaacs/node-glob) from 7.2.0 to 8.0.1.
- [Release notes](https://github.com/isaacs/node-glob/releases)
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v7.2.0...v8.0.1)

---
updated-dependencies:
- dependency-name: glob
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-29 16:16:23 -05:00
Vanathi G
7a94b031e0 Fix char values issue (#13946)
* Fix selection highlight in avatar editor

* Add validation for Fix Character Values fields
2022-04-29 15:57:32 -05:00
SabreCat
22b5a5e6f2 Merge branch 'release' into develop 2022-04-29 14:27:18 -05:00
SabreCat
1d68a95b64 4.229.0 2022-04-29 14:24:24 -05:00
SabreCat
88999a0751 Merge branch 'develop' into release 2022-04-29 14:22:07 -05:00
SabreCat
2666c93e5f chore(css): compile images 2022-04-29 14:21:53 -05:00
Weblate
132918419a Translated using Weblate (Filipino)
Currently translated at 82.0% (110 of 134 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 94.1% (2432 of 2583 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (212 of 212 strings)

Translated using Weblate (Filipino)

Currently translated at 88.0% (118 of 134 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 95.9% (358 of 373 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (690 of 690 strings)

Translated using Weblate (French)

Currently translated at 100.0% (690 of 690 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 93.9% (2426 of 2583 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (690 of 690 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (French)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2583 of 2583 strings)

Translated using Weblate (French)

Currently translated at 99.2% (685 of 690 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 96.2% (664 of 690 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Filipino)

Currently translated at 97.0% (362 of 373 strings)

Translated using Weblate (Filipino)

Currently translated at 97.3% (363 of 373 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 98.2% (677 of 689 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (689 of 690 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 97.5% (672 of 689 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (690 of 690 strings)

Translated using Weblate (Filipino)

Currently translated at 97.3% (363 of 373 strings)

Translated using Weblate (Filipino)

Currently translated at 88.0% (118 of 134 strings)

Translated using Weblate (Filipino)

Currently translated at 97.3% (363 of 373 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.4% (125 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (690 of 690 strings)

Translated using Weblate (Filipino)

Currently translated at 89.5% (120 of 134 strings)

Translated using Weblate (Filipino)

Currently translated at 96.2% (129 of 134 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 96.0% (663 of 690 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 97.2% (670 of 689 strings)

Translated using Weblate (Filipino)

Currently translated at 45.0% (50 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 45.0% (50 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 45.9% (51 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 80.8% (76 of 94 strings)

Translated using Weblate (Filipino)

Currently translated at 90.0% (118 of 131 strings)

Translated using Weblate (Filipino)

Currently translated at 98.6% (368 of 373 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (German)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (German)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (German)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (German)

Currently translated at 100.0% (689 of 689 strings)

Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Bo-Hsiang Chen <rubybhchen@gmail.com>
Co-authored-by: Chap <chalda82+nogravatar@gmail.com>
Co-authored-by: Heriastuti Puteri Utami <putchayviolet@yahoo.co.id>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sciuridae <sweetvshoney@163.com>
Co-authored-by: Thiago Monteiro <thiagoasmonteiro@gmail.com>
Co-authored-by: Vince Vilan <vincemorel.vilan.889@my.csun.edu>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Co-authored-by: 普通学生何某人 <hewei5002@hotmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/id/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/content/de/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/it/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Content
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2022-04-29 06:28:10 +02:00
CuriousMagpie
5faa49d032 2022-05 Subscriber Items 2022-04-28 15:54:09 -04:00
dependabot[bot]
55c800cb4b build(deps): bump stripe from 8.217.0 to 8.219.0 (#13949)
Bumps [stripe](https://github.com/stripe/stripe-node) from 8.217.0 to 8.219.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v8.217.0...v8.219.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-25 12:53:12 -04:00
Natalie L
deee147928 Corrected spelling of Wotchimon and description of rage effect. (#13943) 2022-04-21 16:39:54 -05:00
SabreCat
a9cd36c109 Merge branch 'background-toggle' into develop 2022-04-21 16:24:04 -05:00
SabreCat
4c1c00b0a3 fix(bgs): revert wording change and tokenize 2022-04-21 16:22:36 -05:00
SabreCat
245e135be8 4.228.4 2022-04-21 14:09:48 -05:00
Weblate
978c707e17 Translated using Weblate (German)
Currently translated at 99.7% (687 of 689 strings)

Translated using Weblate (German)

Currently translated at 99.5% (686 of 689 strings)

Translated using Weblate (German)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (German)

Currently translated at 99.9% (2581 of 2583 strings)

Translated using Weblate (German)

Currently translated at 99.2% (684 of 689 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.1% (210 of 214 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.1% (210 of 214 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (212 of 212 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.5% (2494 of 2583 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.5% (194 of 212 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.0% (2456 of 2583 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 96.2% (663 of 689 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (364 of 365 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.8% (2450 of 2583 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (207 of 212 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 95.2% (656 of 689 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 93.7% (2422 of 2583 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (212 of 212 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (212 of 212 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2579 of 2579 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2579 of 2579 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.7% (215 of 220 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (2533 of 2583 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2579 of 2579 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Filipino)

Currently translated at 90.0% (191 of 212 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (219 of 220 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (365 of 365 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (2533 of 2583 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2579 of 2579 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (207 of 212 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (372 of 373 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2583 of 2583 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2583 of 2583 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 93.8% (199 of 212 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 96.8% (212 of 219 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (219 of 219 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (748 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (752 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (752 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (373 of 373 strings)

Co-authored-by: Chap <chalda82+nogravatar@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Heitor Menezes Gomes <heitorgmenezes@gmail.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sciuridae <sweetvshoney@163.com>
Co-authored-by: Vince Vilan <vincemorel.vilan.889@my.csun.edu>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: blacksheep47 <1760906326@qq.com>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Co-authored-by: 普通学生何某人 <hewei5002@hotmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/it/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2022-04-21 21:08:52 +02:00
SabreCat
623d38f281 feat(nav): clicking "Group" goes to first group 2022-04-19 15:38:15 -05:00
dependabot[bot]
c88e458a97 build(deps-dev): bump sinon from 13.0.1 to 13.0.2 (#13940)
Bumps [sinon](https://github.com/sinonjs/sinon) from 13.0.1 to 13.0.2.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v13.0.1...v13.0.2)

---
updated-dependencies:
- dependency-name: sinon
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 16:27:57 -04:00
dependabot[bot]
9957db0669 build(deps): bump nconf from 0.11.4 to 0.12.0 (#13939)
Bumps [nconf](https://github.com/flatiron/nconf) from 0.11.4 to 0.12.0.
- [Release notes](https://github.com/flatiron/nconf/releases)
- [Changelog](https://github.com/indexzero/nconf/blob/master/CHANGELOG.md)
- [Commits](https://github.com/flatiron/nconf/compare/v0.11.4...v0.12.0)

---
updated-dependencies:
- dependency-name: nconf
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 16:21:35 -04:00
dependabot[bot]
2e21f58ae5 build(deps): bump moment from 2.29.2 to 2.29.3 (#13936)
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.3.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/2.29.3/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.3)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 16:20:39 -04:00
dependabot[bot]
04be6d0744 build(deps): bump stripe from 8.216.0 to 8.217.0 (#13935)
Bumps [stripe](https://github.com/stripe/stripe-node) from 8.216.0 to 8.217.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v8.216.0...v8.217.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 16:19:48 -04:00
dependabot[bot]
8c43164e60 build(deps): bump apidoc from 0.51.0 to 0.51.1 (#13934)
Bumps [apidoc](https://github.com/apidoc/apidoc) from 0.51.0 to 0.51.1.
- [Release notes](https://github.com/apidoc/apidoc/releases)
- [Changelog](https://github.com/apidoc/apidoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/apidoc/apidoc/compare/0.51.0...0.51.1)

---
updated-dependencies:
- dependency-name: apidoc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 16:19:28 -04:00
SabreCat
3fa940bdfd Merge remote-tracking branch 'CuriousMagpie/quest-refactors' into develop 2022-04-15 16:35:27 -05:00
Natalie L
3492549081 changed update to updateOne (#13932) 2022-04-15 16:20:37 -05:00
SabreCat
b7a6dd9706 4.228.3 2022-04-15 14:24:08 -05:00
SabreCat
78bdb52f8f Merge branch 'develop' into release 2022-04-15 14:24:02 -05:00
SabreCat
d80aaf3ab0 Merge branch 'develop' into release 2022-04-15 14:23:46 -05:00
negue
31cac936c8 Sprites: re-add previous offsets to sprites (#13933)
* Sprites: re-add previous offsets to sprites

* fix lint
2022-04-15 14:14:48 -05:00
Weblate
3b54064a1f Merge branch 'origin/develop' into Weblate. 2022-04-14 22:34:46 +02:00
Weblate
8b46df757f Translated using Weblate (Spanish (Latin America))
Currently translated at 94.1% (649 of 689 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Tagalog)

Currently translated at 1.1% (2 of 179 strings)

Translated using Weblate (Malayalam)

Currently translated at 60.9% (420 of 689 strings)

Translated using Weblate (Malayalam)

Currently translated at 38.1% (50 of 131 strings)

Translated using Weblate (Tagalog)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Tagalog)

Currently translated at 76.4% (162 of 212 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (2533 of 2583 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (2533 of 2583 strings)

Translated using Weblate (Tagalog)

Currently translated at 75.9% (161 of 212 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 93.0% (641 of 689 strings)

Translated using Weblate (French)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (687 of 689 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (687 of 689 strings)

Translated using Weblate (Tagalog)

Currently translated at 75.9% (161 of 212 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (747 of 755 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.8% (746 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (752 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 87.2% (185 of 212 strings)

Co-authored-by: Chap <chalda82+nogravatar@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: JoanZeppeli <x17501668978@163.com>
Co-authored-by: Linda Li <wli62442@gmail.com>
Co-authored-by: Salman Mujeeb <kingleopard22@gmail.com>
Co-authored-by: Sara López <sarayupy@gmail.com>
Co-authored-by: Sealeo Wu <anitayuanli@gmail.com>
Co-authored-by: Thiago Monteiro <thiagoasmonteiro@gmail.com>
Co-authored-by: Vince Vilan <vincemorel.vilan.889@my.csun.edu>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ml/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ml/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/it/
Translate-URL: https://translate.habitica.com/projects/habitica/front/tl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/tl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/tl/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Content
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Inventory
Translation: Habitica/Questscontent
Translation: Habitica/Settings
2022-04-14 22:30:28 +02:00
SabreCat
4a9040aefb 4.228.2 2022-04-14 15:28:51 -05:00
SabreCat
723249e60d Merge branch 'develop' into release 2022-04-14 15:28:47 -05:00
SabreCat
8b17ebd1f1 fix(settings): don't show email change without email, refresh page 2022-04-14 14:51:11 -05:00
SabreCat
1c4c7b9f1e fix(settings): correct show/hide for email and pass scenarios 2022-04-14 14:21:39 -05:00
SabreCat
58887d9a3c chore(build): ignore non-deployable folders 2022-04-14 13:35:30 -05:00
Phillip Thelen
664f960a8b Pull in missing changes from FB removal PR (#13931)
* fix(auth): hide post hoc Facebook reg

* Pull in missing changes

* fix(lint): whitespace

* fix(strings): missing error message

* fix(tests): update to match functionality

Co-authored-by: SabreCat <sabe@habitica.com>
2022-04-14 12:58:37 -05:00
will yang
dfe53e8b68 Hide Locked Background Year Tab 2022-04-13 17:14:14 -04:00
SabreCat
b608f0ad9c fix(potion): correct year 2022-04-12 16:01:27 -05:00
SabreCat
d4e05835d7 4.228.1 2022-04-12 15:55:02 -05:00
SabreCat
9343c33f37 fix(string): missing April date insert 2022-04-12 15:54:56 -05:00
Weblate
afd1248ea3 Merge branch 'origin/develop' into Weblate. 2022-04-12 21:43:47 +02:00
Weblate
0708829b2a Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (French)

Currently translated at 100.0% (212 of 212 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (372 of 372 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (372 of 372 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 93.4% (2415 of 2583 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (207 of 212 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 92.8% (640 of 689 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.4% (2439 of 2583 strings)

Translated using Weblate (Portuguese)

Currently translated at 84.7% (111 of 131 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.2% (213 of 219 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.0% (2429 of 2583 strings)

Translated using Weblate (Malayalam)

Currently translated at 14.5% (19 of 131 strings)

Translated using Weblate (Arabic)

Currently translated at 92.3% (121 of 131 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.9% (2428 of 2583 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.9% (2428 of 2583 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.9% (2428 of 2583 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (744 of 746 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (371 of 372 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.6% (673 of 689 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.6% (673 of 689 strings)

Translated using Weblate (Japanese)

Currently translated at 96.6% (205 of 212 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.6% (673 of 689 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (688 of 689 strings)

Translated using Weblate (German)

Currently translated at 100.0% (212 of 212 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 92.1% (635 of 689 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2583 of 2583 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2583 of 2583 strings)

Translated using Weblate (Italian)

Currently translated at 98.7% (2552 of 2583 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Italian)

Currently translated at 98.2% (677 of 689 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (219 of 219 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (210 of 210 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (Italian)

Currently translated at 99.5% (209 of 210 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2579 of 2579 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (2574 of 2579 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 88.0% (185 of 210 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.6% (673 of 689 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 93.3% (2412 of 2583 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.2% (130 of 131 strings)

Translated using Weblate (Spanish)

Currently translated at 98.9% (2557 of 2583 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 96.4% (54 of 56 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 83.9% (47 of 56 strings)

Translated using Weblate (Spanish)

Currently translated at 99.5% (209 of 210 strings)

Translated using Weblate (Hindi)

Currently translated at 50.4% (56 of 111 strings)

Translated using Weblate (Arabic)

Currently translated at 68.4% (76 of 111 strings)

Translated using Weblate (Arabic)

Currently translated at 87.5% (113 of 129 strings)

Translated using Weblate (Arabic)

Currently translated at 59.5% (410 of 689 strings)

Translated using Weblate (Spanish)

Currently translated at 98.9% (2556 of 2583 strings)

Translated using Weblate (Italian)

Currently translated at 96.1% (199 of 207 strings)

Translated using Weblate (Spanish)

Currently translated at 98.7% (2550 of 2583 strings)

Translated using Weblate (Spanish)

Currently translated at 99.4% (685 of 689 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (684 of 689 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (684 of 689 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (2573 of 2579 strings)

Translated using Weblate (Malayalam)

Currently translated at 19.8% (22 of 111 strings)

Translated using Weblate (Malayalam)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (German)

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (Spanish)

Currently translated at 98.6% (2548 of 2583 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2579 of 2583 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Spanish)

Currently translated at 99.1% (683 of 689 strings)

Translated using Weblate (German)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (German)

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.5% (672 of 689 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (German)

Currently translated at 99.7% (2577 of 2583 strings)

Translated using Weblate (German)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (German)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (German)

Currently translated at 100.0% (207 of 207 strings)

Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Chap <chalda82+nogravatar@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Heitor Menezes Gomes <heitorgmenezes@gmail.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Mara Dolichotis <marascherzer@gmail.com>
Co-authored-by: PenariaToji <tojipeh@gmail.com>
Co-authored-by: Raithe <RaitheOfDureya@gmail.com>
Co-authored-by: Salman Mujeeb <kingleopard22@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sara López <sarayupy@gmail.com>
Co-authored-by: Vinicius Rodrigues <suburbanizar@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: citrusella <citrusellaflugpucker@yahoo.com>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Co-authored-by: sbrcrbac <fh0f0c9s9@relay.firefox.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ml/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/es/
Translate-URL: https://translate.habitica.com/projects/habitica/front/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/ml/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/it/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/hi/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ml/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/it/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2022-04-12 21:43:37 +02:00
SabreCat
afa9a65933 Merge branch 'release' into develop 2022-04-12 14:41:03 -05:00
SabreCat
e93e91e57d 4.228.0 2022-04-12 14:39:03 -05:00
SabreCat
53569ff983 Merge branch 'sabrecat/wacky-fix' into release 2022-04-12 14:38:41 -05:00
SabreCat
d0c736a7a6 fix(gala): correct dates on seeds and customizations 2022-04-12 14:36:59 -05:00
CuriousMagpie
c8ee51b741 move quest constants to their own folder 2022-04-11 17:10:01 -04:00
CuriousMagpie
2bf63847c9 Merge branch 'quest-refactors' of https://github.com/CuriousMagpie/habitica into quest-refactors 2022-04-11 13:44:15 -04:00
CuriousMagpie
1d1b66d25a Merge remote-tracking branch 'upstream/develop' into quest-refactors 2022-04-11 13:44:03 -04:00
dependabot[bot]
9ce4482040 build(deps): bump winston from 3.6.0 to 3.7.2 (#13927)
Bumps [winston](https://github.com/winstonjs/winston) from 3.6.0 to 3.7.2.
- [Release notes](https://github.com/winstonjs/winston/releases)
- [Changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md)
- [Commits](https://github.com/winstonjs/winston/commits)

---
updated-dependencies:
- dependency-name: winston
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-11 12:38:23 -04:00
dependabot[bot]
28660c0bea build(deps): bump @babel/core from 7.17.8 to 7.17.9 (#13926)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.8 to 7.17.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.9/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-11 12:37:12 -04:00
dependabot[bot]
bc15d530e5 build(deps): bump stripe from 8.215.0 to 8.216.0 (#13925)
Bumps [stripe](https://github.com/stripe/stripe-node) from 8.215.0 to 8.216.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v8.215.0...v8.216.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-11 12:35:01 -04:00
dependabot[bot]
34b7acb246 build(deps): bump nconf from 0.11.3 to 0.11.4 (#13924)
Bumps [nconf](https://github.com/flatiron/nconf) from 0.11.3 to 0.11.4.
- [Release notes](https://github.com/flatiron/nconf/releases)
- [Changelog](https://github.com/indexzero/nconf/blob/master/CHANGELOG.md)
- [Commits](https://github.com/flatiron/nconf/compare/v0.11.3...v0.11.4)

---
updated-dependencies:
- dependency-name: nconf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-11 12:16:00 -04:00
SabreCat
4ef369c6f5 chore(submodule): update habitica-images 2022-04-08 15:45:32 -05:00
SabreCat
405721602f fix(quests): correct events import, prod bug with seasonal availability 2022-04-07 15:51:14 -05:00
SabreCat
19cf89baec fix(pr): remove unrelated change 2022-04-07 15:29:52 -05:00
SabreCat
8a4e9888dd Merge branch 'develop' into quest-refactors 2022-04-07 15:27:19 -05:00
SabreCat
e17b86a1f6 fix(bug-report): relevant change from #13922 2022-04-07 14:48:26 -05:00
negue
2181ab9713 Redesign: "Day Start Adjustment" (#13910)
* Merge Custom Day Start and Timezone into one Component

* ui changes

* change translation text

* typo
2022-04-07 14:37:07 -05:00
negue
8cb8411cc6 Show "Next Hourglass" Month (#13860)
* Show "Next Hourglass" Month

* fix lint

* lint,

* lint

* lint..

* linting bad

* ui fixes

* remove additional margin

* show next hourglass date to debug further

* WIP tests - maybe broken logic

* flex:1 doesn't work - so stats columns now at 33% width

* fix(cron): lint and short circuit

* refactor logic

* update test dates using timezone

* also check for the timezone date

* fix timezone for tests

* fixing the test dates?

* fixing the test dates?

* change nextHourglass logic + update gem cap label / value

* fix lint

* dont add gemsBought to it

* remove tooltip

Co-authored-by: SabreCat <sabe@habitica.com>
2022-04-06 16:30:13 -05:00
SabreCat
da3e6f96ba fix(sprites): move more heavy assets to s3 2022-04-06 14:54:19 -05:00
SabreCat
91511746bc fix(quest): update market criterion 2022-04-06 13:36:43 -05:00
SabreCat
aaba400d36 fix(quest): remove static PNG 2022-04-06 12:42:51 -05:00
CuriousMagpie
4e107d233b fix(quest): update html in questsContent.json 2022-04-06 13:26:15 -04:00
CuriousMagpie
c086d7b91d update sprites 2022-04-06 12:15:39 -05:00
CuriousMagpie
5b93199f35 fix(quest): moving gif into right folder /facepalm 2022-04-06 12:15:05 -05:00
Phillip Thelen
3b82bb9494 Update sprites.css 2022-04-06 12:14:52 -05:00
CuriousMagpie
1c347fc4b8 fix(quest): virtualPet.gif to virtualpet.gig 2022-04-06 12:14:42 -05:00
CuriousMagpie
2f16fe11e1 fix(quest): virtualPet => virtualpet 2022-04-06 12:14:31 -05:00
dependabot[bot]
05cf0cb50d build(deps): bump stripe from 8.212.0 to 8.215.0 (#13919)
Bumps [stripe](https://github.com/stripe/stripe-node) from 8.212.0 to 8.215.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v8.212.0...v8.215.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-06 11:27:13 -04:00
SabreCat
fb017e1f08 fix(pets): various quest and animation corrections 2022-04-05 16:03:46 -05:00
SabreCat
1ca528eb95 Merge branch 'develop' into 2022-04-wacky-potion-quest 2022-04-05 15:08:18 -05:00
SabreCat
0ff2a8c264 4.227.0 2022-04-05 13:55:47 -05:00
Weblate
eed6cfaf6d Translated using Weblate (Japanese)
Currently translated at 99.5% (2567 of 2579 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (2566 of 2579 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (2528 of 2579 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.8% (2524 of 2579 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Hebrew)

Currently translated at 58.9% (33 of 56 strings)

Translated using Weblate (Hebrew)

Currently translated at 84.6% (11 of 13 strings)

Translated using Weblate (Hebrew)

Currently translated at 46.8% (44 of 94 strings)

Translated using Weblate (Hebrew)

Currently translated at 57.6% (64 of 111 strings)

Translated using Weblate (Hebrew)

Currently translated at 73.7% (45 of 61 strings)

Translated using Weblate (Hebrew)

Currently translated at 18.1% (4 of 22 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (2563 of 2579 strings)

Translated using Weblate (Hebrew)

Currently translated at 49.0% (1264 of 2579 strings)

Translated using Weblate (Hebrew)

Currently translated at 70.0% (150 of 214 strings)

Translated using Weblate (Hebrew)

Currently translated at 46.4% (59 of 127 strings)

Translated using Weblate (Hebrew)

Currently translated at 66.9% (249 of 372 strings)

Translated using Weblate (Hebrew)

Currently translated at 71.5% (133 of 186 strings)

Translated using Weblate (Hebrew)

Currently translated at 98.9% (97 of 98 strings)

Translated using Weblate (Hebrew)

Currently translated at 45.0% (307 of 682 strings)

Translated using Weblate (Hebrew)

Currently translated at 67.9% (89 of 131 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.9% (151 of 207 strings)

Translated using Weblate (Hebrew)

Currently translated at 98.5% (132 of 134 strings)

Translated using Weblate (Hebrew)

Currently translated at 91.2% (333 of 365 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Hebrew)

Currently translated at 97.7% (131 of 134 strings)

Translated using Weblate (Hebrew)

Currently translated at 61.9% (122 of 197 strings)

Translated using Weblate (Hebrew)

Currently translated at 98.2% (55 of 56 strings)

Translated using Weblate (Hebrew)

Currently translated at 68.0% (64 of 94 strings)

Translated using Weblate (Hebrew)

Currently translated at 91.8% (102 of 111 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Hebrew)

Currently translated at 93.2% (167 of 179 strings)

Translated using Weblate (Hebrew)

Currently translated at 81.4% (44 of 54 strings)

Translated using Weblate (Hebrew)

Currently translated at 76.2% (569 of 746 strings)

Translated using Weblate (Hebrew)

Currently translated at 74.8% (95 of 127 strings)

Translated using Weblate (Hebrew)

Currently translated at 83.8% (312 of 372 strings)

Translated using Weblate (Hebrew)

Currently translated at 96.7% (180 of 186 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Hebrew)

Currently translated at 69.4% (91 of 131 strings)

Translated using Weblate (Hebrew)

Currently translated at 88.4% (183 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 99.1% (2557 of 2579 strings)

Translated using Weblate (Japanese)

Currently translated at 99.1% (2556 of 2579 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (178 of 179 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (French)

Currently translated at 100.0% (179 of 179 strings)

Translated using Weblate (French)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (French)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 98.9% (2551 of 2579 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2579 of 2579 strings)

Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: JoanZeppeli <x17501668978@163.com>
Co-authored-by: Omer I.S <omeritzicschwartz@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/he/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/he/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/he/
Translate-URL: https://translate.habitica.com/projects/habitica/character/he/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/he/
Translate-URL: https://translate.habitica.com/projects/habitica/content/he/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/he/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/he/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/he/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/he/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/he/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/he/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/he/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/he/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/he/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/he/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/he/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/he/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/he/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/he/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/he/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/he/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Defaulttasks
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2022-04-05 16:35:54 +02:00
CuriousMagpie
202d74b8c4 Virtual Pet animated images 2022-04-04 17:37:09 -04:00
CuriousMagpie
3498b9ccdf Virtual Pet Hatching Potion Quest Content 2022-04-04 17:29:51 -04:00
SabreCat
39b39387d3 chore(sprites): compile CSS 2022-04-04 14:54:09 -05:00
Natalie L
c3278a3baf 2022-04 Backgrounds & Enchanted Armoire Items (#13915)
* 2022-04 Backgrounds & Enchanted Armoire Items

* remove spritesmith-main.css

* adding spritesmith-main.css back in

* css fix
2022-04-04 14:53:34 -05:00
CuriousMagpie
df3b871dba questContent.json placeholders 2022-04-04 14:50:22 -04:00
dependabot[bot]
18af9f8b40 build(deps): bump moment from 2.29.1 to 2.29.2 (#13921)
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.2.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.2)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-04 10:58:05 -04:00
dependabot[bot]
de36afc174 build(deps): bump body-parser from 1.19.2 to 1.20.0 (#13920)
Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.19.2 to 1.20.0.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.19.2...1.20.0)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-04 10:57:45 -04:00
dependabot[bot]
fbb40c0d07 build(deps): bump superagent from 7.1.1 to 7.1.2 (#13917)
Bumps [superagent](https://github.com/visionmedia/superagent) from 7.1.1 to 7.1.2.
- [Release notes](https://github.com/visionmedia/superagent/releases)
- [Changelog](https://github.com/visionmedia/superagent/blob/master/HISTORY.md)
- [Commits](https://github.com/visionmedia/superagent/commits)

---
updated-dependencies:
- dependency-name: superagent
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-04 10:56:57 -04:00
SabreCat
34d0922bcc Merge remote-tracking branch 'rarysson/update-year-on-footer' into develop 2022-04-01 16:28:04 -05:00
Phillip Thelen
55cf2f9795 Improve flows for social auth users (#13862)
* Multiple fixes for social authentication flows

* frontend changes

* add missing computed property

* Improvements to social flows

* fix existing email error

* minor fixes

* fix space

* fix test

* fix lint

Co-authored-by: SabreCat <sabe@habitica.com>
2022-03-31 16:43:16 -05:00
Phillip Thelen
9ff0766910 Fix cron not running if previous run failed (#13892)
* catch issue where cron wouldn’t run bc previous run failed

* add some more tests for cron middleware

* fix lint
2022-03-31 16:32:59 -05:00
Natalie L
06d982401a Update: Revised text string for zodiacZookeeper and birdsOfAFeather achievements (#13884)
* updated text string for zodiacZooKeeper and birdsOfAFeather

* added peacock and rooster (oops!)
2022-03-31 15:59:36 -05:00
citrusella
140372a70d Update faceAvatar.vue so snowman ACTUALLY shows up (#13911) 2022-03-31 15:39:53 -05:00
Weblate
bd9754221b Merge branch 'origin/develop' into Weblate. 2022-03-31 21:48:15 +02:00
SabreCat
d58ca4c998 Merge branch 'release' into develop 2022-03-31 14:47:44 -05:00
SabreCat
7c53f9fd21 4.226.1 2022-03-31 14:47:22 -05:00
SabreCat
ae6918d52e chore(event): canonical duration 2022-03-31 14:41:59 -05:00
Weblate
d367060a82 Translated using Weblate (Japanese)
Currently translated at 98.6% (2543 of 2579 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (French)

Currently translated at 100.0% (197 of 197 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2579 of 2579 strings)

Translated using Weblate (French)

Currently translated at 100.0% (131 of 131 strings)

Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translation: Habitica/Achievements
Translation: Habitica/Gear
Translation: Habitica/Subscriber
2022-03-31 19:36:03 +02:00
Natalie L
8e7ec46c8f Merge branch 'HabitRPG:develop' into develop 2022-03-31 11:58:23 -04:00
Natalie L
d0d32c0b58 fix: Spring Fling Strings (#13902)
* fix: spring fling string fixes

* fix: additional spring fling string fixes
2022-03-30 16:46:35 -05:00
Natalie L
1dd3bc188e Merge branch 'HabitRPG:develop' into develop 2022-03-30 17:04:21 -04:00
SabreCat
608ae5fc43 feat(event): April Fooly 2022 2022-03-30 15:31:17 -05:00
Weblate
391fa541ef Merge branch 'origin/develop' into Weblate. 2022-03-30 16:47:45 +02:00
SabreCat
bc87e8b400 Merge branch 'release' into develop 2022-03-30 09:46:11 -05:00
Weblate
976e8cf242 Translated using Weblate (Spanish)
Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (219 of 219 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (German)

Currently translated at 100.0% (206 of 206 strings)

Co-authored-by: Mara Dolichotis <marascherzer@gmail.com>
Co-authored-by: Pardinus <artemisadlg@gmail.com>
Co-authored-by: Sara López <sarayupy@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/es/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
Translation: Habitica/Achievements
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Settings
2022-03-30 12:18:52 +02:00
Natalie L
2c566cb9e3 Merge branch 'HabitRPG:develop' into develop 2022-03-29 16:26:27 -04:00
CuriousMagpie
89d7f94fc3 habitica-images 2022-03-29 16:21:38 -04:00
Natalie L
7a15bfc140 Merge branch 'HabitRPG:develop' into develop 2022-03-29 16:12:11 -04:00
Weblate
59bd135e44 Merge branch 'origin/develop' into Weblate. 2022-03-29 21:08:20 +02:00
SabreCat
cf04d9d0be Merge branch 'release' into develop 2022-03-29 14:07:57 -05:00
Weblate
f918b4a68c Translated using Weblate (Japanese)
Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (German)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (French)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (French)

Currently translated at 100.0% (219 of 219 strings)

Translated using Weblate (French)

Currently translated at 99.9% (2572 of 2573 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (219 of 219 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (746 of 746 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (130 of 131 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2573 of 2573 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (219 of 219 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (219 of 219 strings)

Translated using Weblate (Ukrainian)

Currently translated at 84.4% (185 of 219 strings)

Translated using Weblate (German)

Currently translated at 100.0% (219 of 219 strings)

Translated using Weblate (German)

Currently translated at 99.9% (2572 of 2573 strings)

Translated using Weblate (Ukrainian)

Currently translated at 72.1% (158 of 219 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (365 of 365 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.0% (171 of 178 strings)

Translated using Weblate (Ukrainian)

Currently translated at 84.9% (634 of 746 strings)

Translated using Weblate (German)

Currently translated at 99.5% (218 of 219 strings)

Translated using Weblate (Ukrainian)

Currently translated at 79.5% (2048 of 2573 strings)

Translated using Weblate (German)

Currently translated at 99.6% (2565 of 2573 strings)

Translated using Weblate (Ukrainian)

Currently translated at 84.9% (634 of 746 strings)

Translated using Weblate (Ukrainian)

Currently translated at 84.9% (634 of 746 strings)

Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Mara Dolichotis <marascherzer@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Sciuridae <sweetvshoney@163.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translation: Habitica/Achievements
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
2022-03-29 19:41:35 +02:00
Natalie L
4070dd7754 Merge branch 'HabitRPG:develop' into develop 2022-03-29 13:15:39 -04:00
Basrara
d44b2fe785 Minor fixes (#13909)
Updated "resetText2", "transaction_rebirth", and "transaction_reroll" to correct capitalization and eliminate a double space.
2022-03-29 12:30:16 -04:00
Natalie L
57cf669a86 Merge branch 'HabitRPG:develop' into develop 2022-03-28 15:01:07 -04:00
CuriousMagpie
f87bb7593e Merge branch 'develop' of https://github.com/CuriousMagpie/habitica into develop 2022-03-25 18:50:14 -04:00
CuriousMagpie
b54f185d1f laptop puked, had to reset everything, joy 2022-03-22 21:02:00 -04:00
CuriousMagpie
1785ac8226 added Birds of a Feather achievement 2022-03-09 15:39:42 -05:00
CuriousMagpie
77179d1ff1 Merge remote-tracking branch 'upstream/develop' into quest-refactors 2022-02-10 11:49:31 -05:00
Rarysson Guilherme
492824ac90 Add automatic year detection on appFooter 2022-02-05 12:26:17 -03:00
CuriousMagpie
55443ecc23 revert files to develop 2022-02-03 12:10:16 -05:00
CuriousMagpie
98b43c681a use spread operator to combine multiple quest objects into one 2022-02-02 16:48:48 -05:00
CuriousMagpie
40f8c049ab concatenate quest constant arrays into a single quests variable 2022-02-02 16:25:28 -05:00
CuriousMagpie
b5a7b58b57 Merge remote-tracking branch 'upstream/develop' into quest-refactors 2022-02-02 16:24:42 -05:00
CuriousMagpie
006aad76d2 first pass on refactoring 2022-01-27 16:59:29 -05:00
CuriousMagpie
1a4af6d6bd Merge remote-tracking branch 'upstream/develop' into quest-refactors 2022-01-27 14:57:41 -05:00
CuriousMagpie
a0b0d1d855 submodule commit 2022-01-27 14:53:52 -05:00
CuriousMagpie
3fb1241010 Merge remote-tracking branch 'upstream/develop' into quest-refactors 2022-01-21 11:55:07 -05:00
CuriousMagpie
f1d70dec18 Merge remote-tracking branch 'upstream/develop' into quest-refactors 2022-01-19 16:59:08 -05:00
Natalie L
c7e7071998 Merge branch 'HabitRPG:develop' into quest-unlocks 2022-01-14 13:58:43 -05:00
CuriousMagpie
b4f699b7c4 added item.locked to buy functions 2022-01-14 13:54:41 -05:00
CuriousMagpie
874954b16b quest unlock logic updates, starting on actually locking quests in shop 2022-01-13 16:36:43 -05:00
CuriousMagpie
120b2e9ade removed a bunch of commented code 2022-01-12 17:07:49 -05:00
CuriousMagpie
7d00fe1ecb more quest logic attempts 2022-01-11 15:42:48 -05:00
CuriousMagpie
29ab8856ca linter fix 2022-01-10 15:21:17 -05:00
CuriousMagpie
1eb8ee4dc6 added a comment re Masterclasser 2022-01-07 21:44:57 -05:00
Natalie L
96c0c12c49 Merge branch 'HabitRPG:develop' into quest-unlocks 2022-01-07 17:37:23 -05:00
CuriousMagpie
3eb9225b8b and more quest logic things to try 2022-01-07 17:36:47 -05:00
CuriousMagpie
73a29f94a6 Merge branch 'quest-unlocks' of https://github.com/CuriousMagpie/habitica into quest-unlocks 2022-01-07 17:34:49 -05:00
CuriousMagpie
7bd190930f more quest logic 2022-01-07 17:33:37 -05:00
CuriousMagpie
d0e9339d3b more quest logic 2022-01-07 17:29:27 -05:00
CuriousMagpie
d45122ce06 lockQuest logic updates 2022-01-07 16:25:31 -05:00
CuriousMagpie
f3f5d6bb70 Merge remote-tracking branch 'upstream/develop' into quest-unlocks 2022-01-07 15:25:08 -05:00
CuriousMagpie
9b849e095c working on quest logic 2022-01-07 15:20:21 -05:00
CuriousMagpie
55ec42678e quest series refactor started 2021-12-31 16:28:04 -05:00
CuriousMagpie
adc7a6ee85 organized quests by type and alphabetically within type 2021-12-31 13:43:59 -05:00
CuriousMagpie
ccc1d5b26e code from abandoned PR #13382 2021-12-29 16:33:11 -05:00
359 changed files with 13705 additions and 7625 deletions

View File

@@ -1,3 +1,7 @@
# Files not included in deployments to Heroku, to save on file size.
/habitica-images
/test
/migrations
/scripts
/database_reports

View File

@@ -21,6 +21,7 @@ RUN npm install -g gulp-cli mocha
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch release --depth 1 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN git config --global url."https://".insteadOf git://
RUN npm set unsafe-perm true
RUN npm install

View File

@@ -20,17 +20,25 @@ function cssVarMap (sprite) {
if (requiresSpecialTreatment) {
sprite.custom = {
px: {
offsetX: `-${sprite.x + 25}px`,
offsetY: `-${sprite.y + 15}px`,
offsetX: '-25px',
offsetY: '-15px',
width: '60px',
height: '60px',
},
};
}
if (sprite.name.indexOf('shirt') !== -1) sprite.custom.px.offsetY = `-${sprite.y + 35}px`; // even more for shirts
// even more for shirts
if (sprite.name.indexOf('shirt') !== -1) {
sprite.custom.px.offsetX = '-29px';
sprite.custom.px.offsetY = '-42px';
}
if (sprite.name.indexOf('hair_base') !== -1) {
const styleArray = sprite.name.split('_').slice(2, 3);
if (Number(styleArray[0]) > 14) sprite.custom.px.offsetY = `-${sprite.y}px`; // don't crop updos
if (Number(styleArray[0]) > 14) {
sprite.custom.px.offsetY = '0'; // don't crop updos
}
}
}

1452
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.226.0",
"version": "4.230.2",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.17.8",
"@babel/preset-env": "^7.16.11",
"@babel/core": "^7.17.10",
"@babel/preset-env": "^7.17.10",
"@babel/register": "^7.17.7",
"@google-cloud/trace-agent": "^5.1.6",
"@parse/node-apn": "^5.1.3",
@@ -13,10 +13,10 @@
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.51.0",
"apidoc": "^0.51.1",
"apple-auth": "^1.0.7",
"bcrypt": "^5.0.1",
"body-parser": "^1.19.2",
"body-parser": "^1.20.0",
"bootstrap": "^4.6.0",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
@@ -27,10 +27,10 @@
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.0.0",
"express": "^4.17.3",
"express": "^4.18.1",
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"glob": "^7.2.0",
"glob": "^8.0.1",
"got": "^11.8.3",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
@@ -43,15 +43,15 @@
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^4.0.2",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.0.5",
"jwks-rsa": "^2.1.1",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.29.1",
"moment": "^2.29.3",
"moment-recur": "^1.0.7",
"mongoose": "^5.13.7",
"morgan": "^1.10.0",
"nconf": "^0.11.3",
"nconf": "^0.12.0",
"node-gcm": "^1.0.5",
"on-headers": "^1.0.2",
"passport": "^0.5.0",
@@ -61,20 +61,20 @@
"paypal-rest-sdk": "^1.8.1",
"pp-ipn": "^1.1.0",
"ps-tree": "^1.0.0",
"rate-limiter-flexible": "^2.3.6",
"rate-limiter-flexible": "^2.3.7",
"redis": "^3.1.2",
"regenerator-runtime": "^0.13.9",
"remove-markdown": "^0.3.0",
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.0",
"stripe": "^8.212.0",
"superagent": "^7.1.1",
"stripe": "^8.222.0",
"superagent": "^7.1.3",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^8.3.2",
"validator": "^13.7.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.6.0",
"winston": "^3.7.2",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.4.23"
},
@@ -110,7 +110,7 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^0.26.1",
"axios": "^0.27.2",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
@@ -122,7 +122,7 @@
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.6",
"sinon": "^13.0.1",
"sinon": "^13.0.2",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
},

View File

@@ -40,7 +40,7 @@ async function deleteHabiticaData (user, email) {
'auth.local.passwordHashMethod': 'bcrypt',
};
if (!user.auth.local.email) set['auth.local.email'] = `${user._id}@example.com`;
await User.update(
await User.updateOne(
{ _id: user._id },
{ $set: set },
);

View File

@@ -99,23 +99,26 @@ describe('Items Utils', () => {
expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5);
});
it('converts values for mounts paths to numbers', () => {
expect(castItemVal('items.mounts.Cactus-Base', 'true')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invisible', 'false')).to.equal(false);
expect(castItemVal('items.mounts.Aether-Invalid', 'true')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invalid', 'truish')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invalid', 0)).to.equal(false);
});
it('converts values for quests paths to numbers', () => {
expect(castItemVal('items.quests.atom3', '5')).to.equal(5);
expect(castItemVal('items.quests.invalid', '5')).to.equal(5);
});
it('converts values for owned gear', () => {
it('converts values for mounts paths to true/null', () => {
// mounts are never false but can be null (function contains more details)
expect(castItemVal('items.mounts.Cactus-Base', 'true')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invisible', 'null')).to.equal(null);
expect(castItemVal('items.mounts.Aether-Invisible', 'false')).to.equal(null);
expect(castItemVal('items.mounts.Aether-Invalid', 'true')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invalid', 'truthy')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invalid', 0)).to.equal(null);
});
it('converts values for owned gear to true/false', () => {
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'thruthy')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
});
});

View File

@@ -128,6 +128,22 @@ describe('cron middleware', () => {
});
});
it('runs cron if previous cron was incomplete', async () => {
user.lastCron = moment(new Date()).subtract({ days: 1 });
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
const now = new Date();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
return resolve();
});
});
});
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
@@ -293,4 +309,33 @@ describe('cron middleware', () => {
});
});
});
it('cron should not run more than once', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
sandbox.spy(cronLib, 'cron');
await Promise.all([new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}), new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}), new Promise((resolve, reject) => {
setTimeout(() => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}, 400);
}),
]);
expect(cronLib.cron).to.be.calledOnce;
});
});

View File

@@ -4,8 +4,7 @@ import {
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import i18n from '../../../../website/common/script/i18n';
import { ensureAdmin, ensureSudo, ensureNewsPoster } from '../../../../website/server/middlewares/ensureAccessRight';
import { ensurePermission } from '../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../website/server/libs/errors';
import apiError from '../../../../website/server/libs/apiError';
@@ -20,20 +19,20 @@ describe('ensure access middlewares', () => {
});
context('ensure admin', () => {
it('returns not authorized when user is not an admin', () => {
res.locals = { user: { contributor: { admin: false } } };
it('returns not authorized when user is not in userSupport', () => {
res.locals = { user: { permissions: { userSupport: false } } };
ensureAdmin(req, res, next);
ensurePermission('userSupport')(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(i18n.t('noAdminAccess'));
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
it('passes when user is an admin', () => {
res.locals = { user: { contributor: { admin: true } } };
it('passes when user is an userSuppor', () => {
res.locals = { user: { permissions: { userSupport: true } } };
ensureAdmin(req, res, next);
ensurePermission('userSupport')(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
@@ -42,40 +41,40 @@ describe('ensure access middlewares', () => {
context('ensure newsPoster', () => {
it('returns not authorized when user is not a newsPoster', () => {
res.locals = { user: { contributor: { newsPoster: false } } };
res.locals = { user: { permissions: { news: false } } };
ensureNewsPoster(req, res, next);
ensurePermission('news')(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(apiError('noNewsPosterAccess'));
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
it('passes when user is a newsPoster', () => {
res.locals = { user: { contributor: { newsPoster: true } } };
res.locals = { user: { permissions: { news: true } } };
ensureNewsPoster(req, res, next);
ensurePermission('news')(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
context('ensure sudo', () => {
it('returns not authorized when user is not a sudo user', () => {
res.locals = { user: { contributor: { sudo: false } } };
context('ensure coupons', () => {
it('returns not authorized when user does not have access to coupon calls', () => {
res.locals = { user: { permissions: { coupons: false } } };
ensureSudo(req, res, next);
ensurePermission('coupons')(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(apiError('noSudoAccess'));
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
it('passes when user is a sudo user', () => {
res.locals = { user: { contributor: { sudo: true } } };
it('passes when user has access to coupon calls', () => {
res.locals = { user: { permissions: { coupons: true } } };
ensureSudo(req, res, next);
ensurePermission('coupons')(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;

View File

@@ -1029,7 +1029,7 @@ describe('Group Model', () => {
expect(toJSON.chat.length).to.equal(1);
});
it('shows messages with >= 2 flag to admins', async () => {
it('shows messages with >= 2 flag to moderators', async () => {
party.chat = [{
flagCount: 3,
info: {
@@ -1037,12 +1037,12 @@ describe('Group Model', () => {
quest: 'basilist',
},
}];
const admin = new User({ 'contributor.admin': true });
const admin = new User({ 'permissions.moderator': true });
const toJSON = await Group.toJSONCleanChat(party, admin);
expect(toJSON.chat.length).to.equal(1);
});
it('doesn\'t show flagged messages to non-admins', async () => {
it('doesn\'t show flagged messages to non-moderators', async () => {
party.chat = [{
flagCount: 3,
info: {

View File

@@ -811,6 +811,16 @@ describe('User Model', () => {
expect(daysMissed).to.eql(5);
});
it('correctly handles a cron that did not complete', () => {
const now = moment();
user.lastCron = moment(now).subtract(2, 'days');
user.auth.timestamps.loggedIn = moment(now).subtract(5, 'days');
const { daysMissed } = user.daysUserHasMissed(now);
expect(daysMissed).to.eql(5);
});
it('uses timezone from preferences to calculate days missed', () => {
const now = moment('2017-07-08 01:00:00Z');
user.lastCron = moment('2017-07-04 13:00:00Z');
@@ -867,7 +877,7 @@ describe('User Model', () => {
expect(user.isNewsPoster()).to.equal(false);
user.contributor.newsPoster = true;
user.permissions = { news: true };
expect(user.isNewsPoster()).to.equal(true);
});

View File

@@ -202,7 +202,7 @@ describe('GET challenges/groups/:groupId', () => {
publicGuild = group;
await user.update({
'contributor.admin': true,
'permissions.challengeAdmin': true,
});
officialChallenge = await generateChallenge(user, group, {

View File

@@ -231,7 +231,7 @@ describe('GET challenges/user', () => {
publicGuild = group;
await user.update({
'contributor.admin': true,
'permissions.challengeAdmin': true,
});
officialChallenge = await generateChallenge(user, group, {

View File

@@ -203,8 +203,8 @@ describe('POST /challenges', () => {
it('sets challenge as official if created by admin and official flag is set', async () => {
await groupLeader.update({
contributor: {
admin: true,
permissions: {
challengeAdmin: true,
},
});

View File

@@ -22,7 +22,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;
userThatDidNotCreateChat = await generateUser();
admin = await generateUser({ 'contributor.admin': true });
admin = await generateUser({ 'permissions.moderator': true });
});
context('Chat errors', () => {

View File

@@ -17,7 +17,7 @@ describe('POST /chat/:chatId/flag', () => {
beforeEach(async () => {
user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ balance: 1, 'contributor.admin': true });
admin = await generateUser({ balance: 1, 'permissions.moderator': true });
anotherUser = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
newUser = await generateUser({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());

View File

@@ -23,7 +23,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
groupWithChat = group;
author = groupLeader;
nonAdmin = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ 'contributor.admin': true });
admin = await generateUser({ 'permissions.moderator': true });
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;

View File

@@ -14,18 +14,18 @@ describe('GET /coupons/', () => {
user = await generateUser();
});
it('returns an error if user has no sudo permission', async () => {
it('returns an error if user has no coupons permission', async () => {
await user.get('/user'); // needed so the request after this will authenticate with the correct cookie session
await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: apiError('noSudoAccess'),
message: apiError('noPrivAccess'),
});
});
it('should return the coupons in CSV format ordered by creation date', async () => {
await user.update({
'contributor.sudo': true,
'permissions.coupons': true,
});
const coupons = await user.post('/coupons/generate/wondercon?count=11');

View File

@@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => {
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'contributor.sudo': true,
'permissions.coupons': true,
});
});

View File

@@ -14,19 +14,19 @@ describe('POST /coupons/generate/:event', () => {
beforeEach(async () => {
user = await generateUser({
'contributor.sudo': true,
'permissions.coupons': true,
});
});
it('returns an error if user has no sudo permission', async () => {
it('returns an error if user has no coupons permission', async () => {
await user.update({
'contributor.sudo': false,
'permissions.coupons': false,
});
await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: apiError('noSudoAccess'),
message: apiError('noPrivAccess'),
});
});
@@ -48,7 +48,7 @@ describe('POST /coupons/generate/:event', () => {
it('should generate coupons', async () => {
await user.update({
'contributor.sudo': true,
'permissions.coupons': true,
});
const coupons = await user.post('/coupons/generate/wondercon?count=2');

View File

@@ -21,7 +21,7 @@ describe('POST /coupons/validate/:code', () => {
it('returns true if coupon code is valid', async () => {
const sudoUser = await generateUser({
'contributor.sudo': true,
'permissions.coupons': true,
});
const [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');

View File

@@ -3,7 +3,7 @@ import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('POST /debug/make-admin (pended for v3 prod testing)', () => {
describe('POST /debug/make-admin', () => {
let user;
before(async () => {
@@ -14,12 +14,12 @@ describe('POST /debug/make-admin (pended for v3 prod testing)', () => {
nconf.set('IS_PROD', false);
});
it('makes user an admine', async () => {
it('makes user an admin', async () => {
await user.post('/debug/make-admin');
await user.sync();
expect(user.contributor.admin).to.eql(true);
expect(user.permissions.fullAccess).to.eql(true);
});
it('returns error when not in production mode', async () => {

View File

@@ -219,11 +219,19 @@ describe('GET /groups', () => {
it('returns 30 guilds per page ordered by number of members', async () => {
await user.update({ balance: 9000 });
const groups = await Promise.all(_.times(60, i => generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
})));
const delay = () => new Promise(resolve => setTimeout(resolve, 40));
const promises = [];
for (let i = 0; i < 60; i += 1) {
promises.push(generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
}));
await delay(); // eslint-disable-line no-await-in-loop
}
const groups = await Promise.all(promises);
// update group number 32 and not the first to make sure sorting works
await groups[32].update({ name: 'guild with most members', memberCount: 199 });

View File

@@ -315,7 +315,7 @@ describe('GET /groups/:id', () => {
beforeEach(async () => {
admin = await generateUser({
'contributor.admin': true,
'permissions.moderator': true,
});
});

View File

@@ -2,6 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
describe('POST /group', () => {
let user;
@@ -203,6 +204,23 @@ describe('POST /group', () => {
expect(updatedUser.balance).to.eql(user.balance - 1);
});
it('does not deduct the gems from user when guild creation fails', async () => {
const stub = sinon.stub(Group.prototype, 'save').rejects();
const promise = user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
await expect(promise).to.eventually.be.rejected;
const updatedUser = await user.get('/user');
expect(updatedUser.balance).to.eql(user.balance);
stub.restore();
});
});
});

View File

@@ -32,7 +32,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
member = members[0]; // eslint-disable-line prefer-destructuring
member2 = members[1]; // eslint-disable-line prefer-destructuring
adminUser = await generateUser({ 'contributor.admin': true });
adminUser = await generateUser({ 'permissions.moderator': true });
});
context('All Groups', () => {

View File

@@ -20,7 +20,7 @@ describe('PUT /group', () => {
},
members: 1,
});
adminUser = await generateUser({ 'contributor.admin': true });
adminUser = await generateUser({ 'permissions.moderator': true });
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0]; // eslint-disable-line prefer-destructuring
@@ -104,11 +104,11 @@ describe('PUT /group', () => {
// Update the bannedWordsAllowed property for the group
const response = await groupLeader.put(`/groups/${group._id}`, updateGroupDetails);
expect(groupLeader.contributor.admin).to.eql(true);
expect(groupLeader.permissions.fullAccess).to.eql(true);
expect(response.bannedWordsAllowed).to.eql(true);
});
it('does not allow for a non-admin to update the bannedWordsAllow property for an existing guild', async () => {
it('does not allow for a non-moderator to update the bannedWordsAllow property for an existing guild', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
@@ -128,7 +128,6 @@ describe('PUT /group', () => {
// Update the bannedWordsAllowed property for the group
const response = await groupLeader.put(`/groups/${group._id}`, updateGroupDetails);
expect(groupLeader.contributor.admin).to.eql(undefined);
expect(response.bannedWordsAllowed).to.eql(undefined);
});
});

View File

@@ -7,9 +7,14 @@ import {
describe('GET /heroes/:heroId', () => {
let user;
const heroFields = [
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret',
];
before(async () => {
user = await generateUser({
contributor: { admin: true },
permissions: { userSupport: true },
});
});
@@ -19,7 +24,7 @@ describe('GET /heroes/:heroId', () => {
await expect(nonAdmin.get(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noAdminAccess'),
message: t('noPrivAccess'),
});
});
@@ -49,10 +54,7 @@ describe('GET /heroes/:heroId', () => {
});
const heroRes = await user.get(`/hall/heroes/${hero._id}`);
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'balance', 'profile', 'purchased',
'contributor', 'auth', 'items', 'secret',
]);
expect(heroRes).to.have.all.keys(heroFields); // works as: object has all and only these keys
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
expect(heroRes.secret.text).to.be.eq('Super Hero');
@@ -64,10 +66,7 @@ describe('GET /heroes/:heroId', () => {
});
const heroRes = await user.get(`/hall/heroes/${hero.auth.local.username}`);
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'balance', 'profile', 'purchased',
'contributor', 'auth', 'items', 'secret',
]);
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
});

View File

@@ -0,0 +1,61 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import apiError from '../../../../../website/server/libs/apiError';
describe('GET /heroes/party/:groupId', () => {
let user; // admin user
before(async () => {
user = await generateUser({
'permissions.userSupport': true,
});
});
it('requires the caller to be an admin', async () => {
const nonAdmin = await generateUser();
const party = await generateGroup(nonAdmin, { type: 'party', privacy: 'private' });
await expect(nonAdmin.get(`/hall/heroes/party/${party._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: apiError('noPrivAccess'),
});
});
it('validates req.params.groupId', async () => {
await expect(user.get('/hall/heroes/party/invalidUUID')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('handles non-existing party', async () => {
const dummyId = generateUUID();
await expect(user.get(`/hall/heroes/party/${dummyId}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('groupWithIDNotFound', { groupId: dummyId }),
});
});
it('returns only necessary party data given group id', async () => {
const nonAdmin = await generateUser();
const party = await generateGroup(nonAdmin, { type: 'party', privacy: 'private' });
const partyRes = await user.get(`/hall/heroes/party/${party._id}`);
expect(partyRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'balance', 'challengeCount', 'leader', 'leaderOnly', 'memberCount',
'purchased', 'quest', 'summary',
]);
expect(partyRes.summary).to.eq(' ');
// NB: 'summary' is NOT a field that the API route retrieves!
// It must not be retrieved for privacy reasons.
// However the group model automatically adds a summary for reasons given here:
// https://github.com/HabitRPG/habitica/blob/8da36bf27c62ba0397a6af260c20d35a17f3d911/website/server/models/group.js#L161-L170
});
});

View File

@@ -1,4 +1,5 @@
import { v4 as generateUUID } from 'uuid';
import { model as User } from '../../../../../website/server/models/user';
import {
generateUser,
translate as t,
@@ -8,15 +9,12 @@ describe('PUT /heroes/:heroId', () => {
let user;
const heroFields = [
'_id', 'balance', 'profile', 'purchased',
'contributor', 'auth', 'items', 'flags',
'secret',
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions',
];
before(async () => {
user = await generateUser({
contributor: { admin: true },
});
user = await generateUser({ 'permissions.userSupport': true });
});
it('requires the caller to be an admin', async () => {
@@ -25,7 +23,7 @@ describe('PUT /heroes/:heroId', () => {
await expect(nonAdmin.put(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noAdminAccess'),
message: t('noPrivAccess'),
});
});
@@ -57,8 +55,7 @@ describe('PUT /heroes/:heroId', () => {
});
// test response
// works as: object has all and only these keys
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes).to.have.all.keys(heroFields); // works as: object has all and only these keys
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
@@ -134,7 +131,6 @@ describe('PUT /heroes/:heroId', () => {
});
// test response
// works as: object has all and only these keys
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
@@ -159,7 +155,6 @@ describe('PUT /heroes/:heroId', () => {
});
// test response
// works as: object has all and only these keys
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
@@ -215,7 +210,6 @@ describe('PUT /heroes/:heroId', () => {
});
// test response
// works as: object has all and only these keys
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
@@ -226,4 +220,35 @@ describe('PUT /heroes/:heroId', () => {
await hero.sync();
expect(hero.items.special.snowball).to.equal(5);
});
it('does not accidentally update API Token', async () => {
// This test has been included because hall.js will contain code to produce
// a truncated version of the API Token, and we want to be sure that
// the real Token is not modified by bugs in that code.
const hero = await generateUser();
const originalToken = hero.apiToken;
// make any change to the user except the Token
await user.put(`/hall/heroes/${hero._id}`, {
contributor: { text: 'Astronaut' },
});
const updatedHero = await User.findById(hero._id).exec();
expect(updatedHero.apiToken).to.equal(originalToken);
expect(updatedHero.apiTokenObscured).to.not.exist;
});
it('does update API Token when admin changes it', async () => {
const hero = await generateUser();
const originalToken = hero.apiToken;
// change the user's API Token
await user.put(`/hall/heroes/${hero._id}`, {
changeApiToken: true,
});
const updatedHero = await User.findById(hero._id).exec();
expect(updatedHero.apiToken).to.not.equal(originalToken);
expect(updatedHero.apiTokenObscured).to.not.exist;
});
});

View File

@@ -176,7 +176,7 @@ describe('POST /members/send-private-message', () => {
it('allows admin to send when sender has blocked the admin', async () => {
userToSendMessage = await generateUser({
'contributor.admin': 1,
'permissions.moderator': true,
});
const receiver = await generateUser({ 'inbox.blocks': [userToSendMessage._id] });
@@ -204,7 +204,7 @@ describe('POST /members/send-private-message', () => {
it('allows admin to send when to user has opted out of messaging', async () => {
userToSendMessage = await generateUser({
'contributor.admin': 1,
'permissions.moderator': true,
});
const receiver = await generateUser({ 'inbox.optOut': true });

View File

@@ -105,7 +105,7 @@ describe('GET /tasks/:id', () => {
it('can get challenge task if admin', async () => {
const admin = await generateUser({
'contributor.admin': true,
'permissions.challengeAdmin': true,
});
const getTask = await admin.get(`/tasks/${task._id}`);

View File

@@ -60,7 +60,7 @@ describe('POST /tasks/challenge/:challengeId', () => {
});
it('allows non-leader admin to add tasks to a challenge when not a member', async () => {
const admin = await generateUser({ 'contributor.admin': true });
const admin = await generateUser({ 'permissions.challengeAdmin': true });
const task = await admin.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit from admin',
type: 'habit',

View File

@@ -120,7 +120,7 @@ describe('POST /user/reset', () => {
it('does not delete secret', async () => {
const admin = await generateUser({
contributor: { admin: true },
permissions: { userSupport: true },
});
const hero = await generateUser({

View File

@@ -135,6 +135,7 @@ describe('PUT /user', () => {
'gem balance': { balance: 100 },
auth: { 'auth.blocked': true, 'auth.timestamps.created': new Date() },
contributor: { 'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text' },
permissions: { 'permissions.fullAccess': true, 'permissions.news': true, 'permissions.moderator': 'some text' },
backer: { 'backer.tier': 10, 'backer.npc': 'Bilbo' },
subscriptions: { 'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000 },
'customization gem purchases': { 'purchased.background.tavern': true, 'purchased.skin.bear': true },

View File

@@ -1,3 +1,4 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
requester,
@@ -9,15 +10,18 @@ describe('GET /user/auth/apple', () => {
let api;
let user;
const appleEndpoint = '/user/auth/apple';
before(async () => {
const expectedResult = { id: 'appleId', name: 'an apple user' };
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
});
let randomAppleId = '123456';
beforeEach(async () => {
api = requester();
user = await generateUser();
randomAppleId = generateUUID();
const expectedResult = { id: randomAppleId, name: 'an apple user' };
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
});
afterEach(async () => {
appleAuth.appleProfile.restore();
});
it('registers a new user', async () => {
@@ -26,7 +30,7 @@ describe('GET /user/auth/apple', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal('appleId');
await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal(randomAppleId);
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('an apple user');
});

View File

@@ -1,4 +1,5 @@
import passport from 'passport';
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
requester,
@@ -10,14 +11,15 @@ describe('POST /user/auth/social', () => {
let api;
let user;
const endpoint = '/user/auth/social';
const randomAccessToken = '123456';
const facebookId = 'facebookId';
const googleId = 'googleId';
let randomAccessToken = '123456';
let randomFacebookId = 'facebookId';
let randomGoogleId = 'googleId';
let network = 'NoNetwork';
beforeEach(async () => {
api = requester();
user = await generateUser();
randomAccessToken = generateUUID();
});
it('fails if network is not supported', async () => {
@@ -32,12 +34,23 @@ describe('POST /user/auth/social', () => {
});
describe('facebook', () => {
before(async () => {
const expectedResult = { id: facebookId, displayName: 'a facebook user' };
beforeEach(async () => {
randomFacebookId = generateUUID();
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: `${user.auth.local.username}+facebook@example.com` },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
network = 'facebook';
});
afterEach(async () => {
passport._strategies.facebook.userProfile.restore();
});
it('registers a new user', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -51,7 +64,8 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId);
await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+facebook@example.com`);
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(randomFacebookId);
});
it('logs an existing user in', async () => {
@@ -68,6 +82,57 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false;
expect(registerResponse.newUser).to.be.true;
});
it('logs an existing user in if they have local auth with matching email', async () => {
passport._strategies.facebook.userProfile.restore();
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('logs an existing user into their social account if they have local auth with matching email', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
// This is important for existing accounts before the new social handling
passport._strategies.facebook.userProfile.restore();
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.apiToken).not.to.eql(user.apiToken);
expect(response.id).not.to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('add social auth to an existing user', async () => {
@@ -76,11 +141,28 @@ describe('POST /user/auth/social', () => {
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
await expect(user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('socialAlreadyExists'),
});
});
xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -92,12 +174,23 @@ describe('POST /user/auth/social', () => {
});
describe('google', () => {
before(async () => {
const expectedResult = { id: googleId, displayName: 'a google user' };
beforeEach(async () => {
randomGoogleId = generateUUID();
const expectedResult = {
id: randomGoogleId,
displayName: 'a google user',
emails: [
{ value: `${user.auth.local.username}+google@example.com` },
],
};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
network = 'google';
});
afterEach(async () => {
passport._strategies.google.userProfile.restore();
});
it('registers a new user', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -107,7 +200,8 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(googleId);
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(randomGoogleId);
await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+google@example.com`);
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
@@ -125,6 +219,57 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false;
expect(registerResponse.newUser).to.be.true;
});
it('logs an existing user in if they have local auth with matching email', async () => {
passport._strategies.google.userProfile.restore();
const expectedResult = {
id: randomGoogleId,
displayName: 'a google user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('logs an existing user into their social account if they have local auth with matching email', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
// This is important for existing accounts before the new social handling
passport._strategies.google.userProfile.restore();
const expectedResult = {
id: randomGoogleId,
displayName: 'a google user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.apiToken).not.to.eql(user.apiToken);
expect(response.id).not.to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('add social auth to an existing user', async () => {
@@ -133,11 +278,28 @@ describe('POST /user/auth/social', () => {
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
await expect(user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('socialAlreadyExists'),
});
});
xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase

View File

@@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => {
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'contributor.sudo': true,
'permissions.coupons': true,
});
});

View File

@@ -8,7 +8,7 @@ describe('GET /members/:memberId/purchase-history', () => {
before(async () => {
user = await generateUser({
contributor: { admin: true },
permissions: { userSupport: true },
});
});
@@ -26,7 +26,7 @@ describe('GET /members/:memberId/purchase-history', () => {
await expect(nonAdmin.get(`/members/${member._id}/purchase-history`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noAdminAccess'),
message: t('noPrivAccess'),
});
});

View File

@@ -15,16 +15,16 @@ describe('DELETE /news/:newsID', () => {
};
beforeEach(async () => {
user = await generateUser({
'contributor.newsPoster': true,
'permissions.news': true,
});
});
it('disallows access to non-newsPosters', async () => {
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
const nonAdminUser = await generateUser({ 'permissions.news': false });
await expect(nonAdminUser.del(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: 'You don\'t have news poster access.',
message: t('noPrivAccess'),
});
});

View File

@@ -15,7 +15,7 @@ describe('GET /news', () => {
before(async () => {
api = requester();
const user = await generateUser({
'contributor.newsPoster': true,
'permissions.news': true,
});
await Promise.all([

View File

@@ -15,7 +15,7 @@ describe('GET /news/:newsID', () => {
};
beforeEach(async () => {
user = await generateUser({
'contributor.newsPoster': true,
'permissions.news': true,
});
});

View File

@@ -16,16 +16,16 @@ describe('POST /news', () => {
};
beforeEach(async () => {
user = await generateUser({
'contributor.newsPoster': true,
'permissions.news': true,
});
});
it('disallows access to non-admins', async () => {
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
const nonAdminUser = await generateUser({ 'permissions.news': false });
await expect(nonAdminUser.post('/news')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: 'You don\'t have news poster access.',
message: 'You don\'t have the required privileges.',
});
});

View File

@@ -17,16 +17,16 @@ describe('PUT /news/:newsID', () => {
};
beforeEach(async () => {
user = await generateUser({
'contributor.newsPoster': true,
'permissions.news': true,
});
});
it('disallows access to non-admins', async () => {
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
const nonAdminUser = await generateUser({ 'permissions.news': false });
await expect(nonAdminUser.put('/news/1234')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: 'You don\'t have news poster access.',
message: 'You don\'t have the required privileges.',
});
});

View File

@@ -120,7 +120,7 @@ describe('POST /user/reset', () => {
it('does not delete secret', async () => {
const admin = await generateUser({
contributor: { admin: true },
permissions: { userSupport: true },
});
const hero = await generateUser({

View File

@@ -84,6 +84,7 @@ describe('PUT /user', () => {
'gem balance': { balance: 100 },
auth: { 'auth.blocked': true, 'auth.timestamps.created': new Date() },
contributor: { 'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text' },
permissions: { 'permissions.fullAccess': true, 'permissions.news': true, 'permissions.moderator': 'some text' },
backer: { 'backer.tier': 10, 'backer.npc': 'Bilbo' },
subscriptions: { 'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000 },
'customization gem purchases': { 'purchased.background.tavern': true, 'purchased.skin.bear': true },

View File

@@ -1,6 +1,6 @@
import moment from 'moment';
import { startOfDay, daysSince } from '../../../website/common/script/cron';
import { startOfDay, daysSince, getPlanContext } from '../../../website/common/script/cron';
function localMoment (timeString, utcOffset) {
return moment(timeString).utcOffset(utcOffset, true);
@@ -181,4 +181,63 @@ describe('cron utility functions', () => {
expect(result).to.equal(0);
});
});
describe('getPlanContext', () => {
const now = new Date(2022, 5, 1);
function baseUserData (count, offset, planId) {
return {
purchased: {
plan: {
consecutive: {
count,
offset,
gemCapExtra: 25,
trinkets: 19,
},
quantity: 1,
extraMonths: 0,
gemsBought: 0,
owner: '116b4133-8fb7-43f2-b0de-706621a8c9d8',
nextBillingDate: null,
nextPaymentProcessing: null,
planId,
customerId: 'group-plan',
dateUpdated: '2022-05-10T03:00:00.144+01:00',
paymentMethod: 'Group Plan',
dateTerminated: null,
lastBillingDate: null,
dateCreated: '2017-02-10T19:00:00.355+01:00',
},
},
};
}
it('offset 0, next date in 3 months', () => {
const user = baseUserData(60, 0, 'group_plan_auto');
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-08-10T02:00:00.144Z');
});
it('offset 1, next date in 1 months', () => {
const user = baseUserData(60, 1, 'group_plan_auto');
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
});
it('offset 2, next date in 2 months - with any plan', () => {
const user = baseUserData(60, 2, 'basic_3mo');
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
});
});
});

View File

@@ -1,4 +1,5 @@
import { v4 as generateUUID } from 'uuid';
import getters from '@/store/getters';
export const userStyles = {
contributor: {
@@ -82,3 +83,25 @@ export const userStyles = {
classSelected: true,
},
};
export function mockStore ({
userData,
...state
}) {
return {
getters,
dispatch: () => {
},
watch: () => {
},
state: {
user: {
data: {
...userData,
},
},
...state,
},
};
}

View File

@@ -1,36 +1,46 @@
.quest_lostMasterclasser4 {
background: url("~@/assets/images/animated/quest_lostMasterclasser4.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_windup {
background: url("~@/assets/images/animated/quest_windup.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_solarSystem {
background: url("~@/assets/images/animated/quest_solarSystem.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat;
width: 219px;
height: 219px;
}
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup {
.quest_virtualpet {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat;
width: 219px;
height: 219px;
}
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, .Pet_HatchingPotion_VirtualPet {
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Dessert {
background: url("~@/assets/images/animated/Pet_HatchingPotion_Dessert.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat;
}
.Pet_HatchingPotion_Veggie {
background: url("~@/assets/images/animated/Pet_HatchingPotion_Veggie.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
}
.Pet_HatchingPotion_Windup {
background: url("~@/assets/images/animated/Pet_HatchingPotion_Windup.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat;
}
.Pet_HatchingPotion_VirtualPet {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat;
}
.Gems {
@@ -68,7 +78,7 @@
/* Critical */
.weapon_special_critical {
background: url("~@/assets/images/animated/weapon_special_critical.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
width: 90px;
height: 90px;
margin-left:-12px;
@@ -85,32 +95,32 @@
}
.head_special_0 {
background: url("~@/assets/images/animated/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
}
.head_special_1 {
background: url("~@/assets/images/animated/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
margin-top: 3px;
}
.broad_armor_special_0,.slim_armor_special_0 {
background: url("~@/assets/images/animated/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
}
.broad_armor_special_1,.slim_armor_special_1 {
background: url("~@/assets/images/animated/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
}
.shield_special_0 {
background: url("~@/assets/images/animated/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
}
.weapon_special_0 {
background: url("~@/assets/images/animated/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
}
.Pet-Wolf-Cerberus {
width: 105px;
height: 72px;
background: url("~@/assets/images/animated/BackerOnly-Pet-CerberusPup.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat;
}
.broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
@@ -119,29 +129,29 @@
}
.broad_armor_special_ks2019, .slim_armor_special_ks2019 {
background: url("~@/assets/images/animated/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
}
.eyewear_special_ks2019 {
background: url("~@/assets/images/animated/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
}
.head_special_ks2019 {
background: url("~@/assets/images/animated/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
}
.shield_special_ks2019 {
background: url("~@/assets/images/animated/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
}
.weapon_special_ks2019 {
background: url("~@/assets/images/animated/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
width: 120px;
height: 120px;
}
.Pet-Gryphon-Gryphatrice {
background: url("~@/assets/images/animated/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
width: 81px;
height: 99px;
}
@@ -152,11 +162,11 @@
}
.Mount_Head_Gryphon-Gryphatrice {
background: url("~@/assets/images/animated/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
}
.Mount_Body_Gryphon-Gryphatrice {
background: url("~@/assets/images/animated/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
}
.background_airship, .background_clocktower, .background_steamworks {
@@ -165,15 +175,15 @@
}
.background_airship {
background: url("~@/assets/images/animated/background_airship.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat;
}
.background_clocktower {
background: url("~@/assets/images/animated/background_clocktower.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat;
}
.background_steamworks {
background: url("~@/assets/images/animated/background_steamworks.gif") no-repeat;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
}
/* FIXME figure out how to handle customize menu!!

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

View File

@@ -19,6 +19,11 @@
top: -16px !important;
}
.Pet.Pet-FlyingPig-Veggie, .Pet.Pet-FlyingPig-Dessert {
.Pet.Pet-FlyingPig-Veggie, .Pet.Pet-FlyingPig-Dessert, .Pet.Pet-FlyingPig-VirtualPet {
top: -28px !important;
}
.Pet[class*="Virtual"] {
left: 1.25rem;
bottom: 0.5rem;
}

View File

@@ -0,0 +1,7 @@
import moment from 'moment';
export default function formatDate (inputDate) {
if (!inputDate) return '';
const date = moment(inputDate).utcOffset(0).format('YYYY-MM-DD HH:mm');
return `${date} UTC`;
}

View File

@@ -0,0 +1,81 @@
<template>
<div class="row standard-page">
<div class="well col-12">
<h1>Admin Panel</h1>
<div>
<form
class="form-inline"
@submit.prevent="loadHero(userIdentifier)"
>
<input
v-model="userIdentifier"
class="form-control uidField"
type="text"
:placeholder="'User ID or Username; blank for your account'"
>
<input
type="submit"
value="Load User"
class="btn btn-secondary"
>
</form>
</div>
<div>
<router-view @changeUserIdentifier="changeUserIdentifier" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.uidField {
min-width: 45ch;
}
</style>
<script>
import VueRouter from 'vue-router';
import { mapState } from '@/libs/store';
const { isNavigationFailure, NavigationFailureType } = VueRouter;
export default {
data () {
return {
userIdentifier: '',
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: 'Admin Panel',
});
},
methods: {
changeUserIdentifier (newId) {
// If we've accessed the admin panel from a URL that had a user identifier in it,
// this method will insert that identifier into the "Load User" form field
// (useful if we want to re-fetch the user after making changes).
this.userIdentifier = newId;
},
async loadHero (userIdentifier) {
const id = userIdentifier || this.user._id;
this.$router.push({
name: 'adminPanelUser',
params: { userIdentifier: id },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
// the admin has requested that the same user be displayed again so reload the page
// (e.g., if they changed their mind about changes they were making)
this.$router.go();
}
});
},
},
};
</script>

View File

@@ -0,0 +1,132 @@
import content from '@/../../common/script/content';
function _getGearSetName (key) {
let set = 'NO SET [probably an omission in the API data]';
if (content.gear.flat[key].set) {
set = `${content.gear.flat[key].set}`;
}
return set;
}
function _getGearSetDescription (key) {
let setName = _getGearSetName(key);
if (setName === 'special-takeThis') {
// no point displaying set details for gear where it's obvious
return '';
}
const klassNames = {
healer: 'Healer',
rogue: 'Rogue',
warrior: 'Warrior',
wizard: 'Mage',
};
const lunarBattleQuestGear = ['armor_special_lunarWarriorArmor', 'head_special_lunarWarriorHelm', 'weapon_special_lunarScythe'];
const loginIncentivesGear = ['armor_special_bardRobes', 'armor_special_dandySuit', 'armor_special_lunarWarriorArmor', 'armor_special_nomadsCuirass', 'armor_special_pageArmor', 'armor_special_samuraiArmor', 'armor_special_sneakthiefRobes', 'armor_special_snowSovereignRobes', 'back_special_snowdriftVeil', 'head_special_bardHat', 'head_special_clandestineCowl', 'head_special_dandyHat', 'head_special_kabuto', 'head_special_lunarWarriorHelm', 'head_special_pageHelm', 'head_special_snowSovereignCrown', 'head_special_spikedHelm', 'shield_special_diamondStave', 'shield_special_lootBag', 'shield_special_wakizashi', 'shield_special_wintryMirror', 'weapon_special_bardInstrument', 'weapon_special_fencingFoil', 'weapon_special_lunarScythe', 'weapon_special_nomadsScimitar', 'weapon_special_pageBanner', 'weapon_special_skeletonKey', 'weapon_special_tachi'];
const goldQuestsGear = ['armor_special_finnedOceanicArmor', 'head_special_fireCoralCirclet', 'weapon_special_tridentOfCrashingTides', 'shield_special_moonpearlShield', 'head_special_pyromancersTurban', 'armor_special_pyromancersRobes', 'weapon_special_taskwoodsLantern', 'armor_special_mammothRiderArmor', 'head_special_mammothRiderHelm', 'weapon_special_mammothRiderSpear', 'shield_special_mammothRiderHorn', 'armor_special_roguishRainbowMessengerRobes', 'head_special_roguishRainbowMessengerHood', 'weapon_special_roguishRainbowMessage', 'shield_special_roguishRainbowMessage', 'eyewear_special_aetherMask', 'body_special_aetherAmulet', 'back_special_aetherCloak', 'weapon_special_aetherCrystals'];
const animalGear = ['back_special_bearTail', 'back_special_cactusTail', 'back_special_foxTail', 'back_special_lionTail', 'back_special_pandaTail', 'back_special_pigTail', 'back_special_tigerTail', 'back_special_wolfTail', 'headAccessory_special_bearEars', 'headAccessory_special_cactusEars', 'headAccessory_special_foxEars', 'headAccessory_special_lionEars', 'headAccessory_special_pandaEars', 'headAccessory_special_pigEars', 'headAccessory_special_tigerEars', 'headAccessory_special_wolfEars'];
let wantSetName = true; // some set names are useful, others aren't
let setType = '[cannot determine set type]';
if (setName === 'base-0') {
setType = 'empty slot';
wantSetName = false;
} else if (setName.includes('special-turkey')) {
setType = '<a href="https://habitica.fandom.com/wiki/Turkey_Day">Turkey Day</a>';
wantSetName = false;
} else if (setName.includes('special-nye')) {
setType = '<a href="https://habitica.fandom.com/wiki/Event_Item_Sequences">New Year\'s Eve</a>';
wantSetName = false;
} else if (setName.includes('special-birthday')) {
setType = '<a href="https://habitica.fandom.com/wiki/Habitica_Birthday_Bash">Habitica Birthday Bash</a>';
wantSetName = false;
} else if (setName.includes('special-0') || key === 'weapon_special_3') {
setType = '<a href="https://habitica.fandom.com/wiki/Kickstarter">Kickstarter 2013</a>';
wantSetName = false;
} else if (setName.includes('special-1')) {
setType = 'Contributor gear';
wantSetName = false;
} else if (setName.includes('special-2') || key === 'shield_special_goldenknight') {
setType = '<a href="https://habitica.fandom.com/wiki/Legendary_Equipment">Legendary Equipment</a>';
wantSetName = false;
} else if (setName.includes('special-wondercon')) {
setType = '<a href="https://habitica.fandom.com/wiki/Unconventional_Armor">Unconventional Armor</a>';
wantSetName = false;
} else if (lunarBattleQuestGear.includes(key)) {
setType = '<a href="https://habitica.fandom.com/wiki/Quest_Lines#Lunar_Battle_Quest_Line">Lunar Battle Quest Line</a>';
wantSetName = false;
} else if (loginIncentivesGear.includes(key)) {
setType = '<a href="https://habitica.fandom.com/wiki/Daily_Check-In_Incentives">Check-In Incentive</a>';
wantSetName = false;
} else if (goldQuestsGear.includes(key)) {
setType = 'from <a href="https://habitica.fandom.com/wiki/Quest_Lines#Gold_Purchasable_Quest_Lines">Gold-Purchasable Quest Lines</a>';
wantSetName = false;
} else if (animalGear.includes(key)) {
setType = '<a href="https://habitica.fandom.com/wiki/Avatar_Customizations">Animal Avatar Accessory Customisations</a>';
wantSetName = false;
} else if (!content.gear.flat[key].klass) {
setType = 'NO "klass" [omission in API data]';
} else if (content.gear.flat[key].klass === 'armoire') {
setType = 'Armoire set';
} else if (content.gear.flat[key].klass === 'mystery') {
setType = 'Mystery Items';
setName = setName.replace(/mystery-(....)(..)/, '$1-$2');
} else if (content.gear.flat[key].klass === 'special') {
const specialClass = content.gear.flat[key].specialClass || '';
if (specialClass && Object.keys(klassNames).includes(specialClass)) {
setType = `Grand Gala ${klassNames[specialClass]} set`;
} else if (key.includes('special_gaymerx')) {
setType = 'GaymerX';
wantSetName = false;
} else if (key.includes('special_ks2019')) {
setType = '<a href="https://habitica.fandom.com/wiki/Kickstarter">Kickstarter 2019</a>';
wantSetName = false;
} else {
setType = '[unknown set]';
wantSetName = false;
}
} else if (Object.keys(klassNames).includes(content.gear.flat[key].klass)) {
// e.g., base class gear such as weapon_warrior_6 (Golden Sword)
setType = `base ${klassNames[content.gear.flat[key].klass]} gear`;
wantSetName = false;
}
return (wantSetName) ? `${setType}: ${setName}` : setType;
}
export default {
data () {
return {
content,
};
},
methods: {
getItemDescription (itemType, key) {
// Returns item name. Also returns other info for equipment.
const simpleItemTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'special'];
if (simpleItemTypes.includes(itemType) && content[itemType][key]) {
return content[itemType][key].text();
}
if (itemType === 'mounts' && content.mountInfo[key]) {
return content.mountInfo[key].text();
}
if (itemType === 'pets' && content.petInfo[key]) {
return content.petInfo[key].text();
}
if (itemType === 'gear' && content.gear.flat[key]) {
const name = content.gear.flat[key].text();
const description = _getGearSetDescription(key);
if (description) return `${name} -- ${description}`;
return name;
}
return 'NO NAME - invalid item?';
},
},
};

View File

@@ -0,0 +1,20 @@
export default {
methods: {
async saveHero ({ hero, msg = 'User', clearData }) {
await this.$store.dispatch('hall:updateHero', { heroDetails: hero });
await this.$store.dispatch('snackbars:add', {
title: '',
text: `${msg} updated`,
type: 'info',
});
if (clearData) {
// Use clearData when the saved changes may affect data in other components
// (e.g., adding a contributor tier will increase the Gem balance)
// The admin should re-fetch the data if they need to keep working on that user.
this.$emit('clear-data');
this.$router.push({ name: 'adminPanel' });
}
},
},
};

View File

@@ -0,0 +1,68 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Current Avatar Appearance, Drop Count Today
</h3>
<div v-if="expand">
<div>Drops Today: {{ items.lastDrop.count }}</div>
<div>Most Recent Drop: {{ items.lastDrop.date | formatDate }}</div>
<div>Use Costume: {{ preferences.costume ? 'on' : 'off' }}</div>
<div class="subsection-start">
Equipped Gear:
<ul v-html="formatEquipment(items.gear.equipped)"></ul>
</div>
<div>
Costume:
<ul v-html="formatEquipment(items.gear.costume)"></ul>
</div>
</div>
</div>
</template>
<script>
import formatDate from '../filters/formatDate';
import getItemDescription from '../mixins/getItemDescription';
export default {
filters: {
formatDate,
},
mixins: [
getItemDescription,
],
props: {
items: {
type: Object,
required: true,
},
preferences: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
};
},
methods: {
formatEquipment (gearWorn) {
const gearTypes = ['head', 'armor', 'weapon', 'shield', 'headAccessory', 'eyewear',
'body', 'back'];
let equipmentList = '';
gearTypes.forEach(gearType => {
const key = gearWorn[gearType] || '';
const description = (key)
? `<strong>${key}</strong> : ${this.getItemDescription('gear', gearWorn[gearType])}`
: 'none';
equipmentList += `<li>${gearType} : ${description}</li>\n`;
});
return equipmentList;
},
},
};
</script>

View File

@@ -0,0 +1,34 @@
<template>
<div>
<h2>@{{ auth.local.username }} &nbsp; / &nbsp; {{ profile.name }}</h2>
{{ userId }} &nbsp;
<router-link :to="{'name': 'userProfile', 'params': {'userId': userId}}">
profile link
</router-link>
<br>
language: {{ preferences.language }}
</div>
</template>
<script>
export default {
props: {
userId: {
type: String,
required: true,
},
auth: {
type: Object,
required: true,
},
preferences: {
type: Object,
required: true,
},
profile: {
type: Object,
required: true,
},
},
};
</script>

View File

@@ -0,0 +1,206 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Contributor Details
</h3>
<div v-if="expand">
<form @submit.prevent="saveHero({hero, msg: 'Contributor details', clearData: true})">
<div>
<label>Permissions</label>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.fullAccess"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
Full Admin Access (Allows access to everything. EVERYTHING)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.userSupport"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
User Support (Access this form, access purchase history)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.news"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
News poster (Bailey CMS)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.moderator"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
Community Moderator (ban and mute users, access chat flags, manage social spaces)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.challengeAdmin"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
Challenge Admin (can create official habitica challenges and admin all challenges)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.coupons"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
Coupon Creator (can manage coupon codes)
</label>
</div>
</div>
<div class="form-group">
<label>Title</label>
<input
v-model="hero.contributor.text"
class="form-control textField"
type="text"
>
<small>
Common titles:
<strong>Ambassador, Artisan, Bard, Blacksmith, Challenger, Comrade, Fletcher,
Linguist, Linguistic Scribe, Scribe, Socialite, Storyteller</strong>.
<br>
Rare titles:
Advisor, Chamberlain, Designer, Mathematician, Shirtster, Spokesperson,
Statistician, Tinker, Transcriber, Troubadour.
</small>
</div>
<div class="form-group form-inline">
<label>Tier</label>
<input
v-model="hero.contributor.level"
class="form-control levelField"
type="number"
>
<small>
1-7 for normal contributors, 8 for moderators, 9 for staff.
This determines which items, pets, mounts are available, and name-tag coloring.
Tiers 8 and 9 are automatically given admin status.
</small>
</div>
<div
v-if="hero.secret.text"
class="form-group"
>
<label>Moderation Notes</label>
<div
v-markdown="hero.secret.text"
class="markdownPreview"
></div>
</div>
<div class="form-group">
<label>Contributions</label>
<textarea
v-model="hero.contributor.contributions"
class="form-control"
cols="5"
rows="5"
></textarea>
<div
v-markdown="hero.contributor.contributions"
class="markdownPreview"
></div>
</div>
<div class="form-group">
<label>Edit Moderation Notes</label>
<textarea
v-model="hero.secret.text"
class="form-control"
cols="5"
rows="3"
></textarea>
<div
v-markdown="hero.secret.text"
class="markdownPreview"
></div>
</div>
<input
type="submit"
value="Save and Clear Data"
class="btn btn-primary"
>
</form>
</div>
</div>
</template>
<style lang="scss" scoped>
.levelField {
min-width: 10ch;
}
.textField {
min-width: 50ch;
}
</style>
<script>
import markdownDirective from '@/directives/markdown';
import saveHero from '../mixins/saveHero';
import { mapState } from '@/libs/store';
import { userStateMixin } from '../../../mixins/userState';
function resetData (self) {
self.expand = self.hero.contributor.level;
}
export default {
directives: {
markdown: markdownDirective,
},
mixins: [
userStateMixin,
saveHero,
],
computed: {
...mapState({ user: 'user.data' }),
},
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
};
</script>

View File

@@ -0,0 +1,223 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Timestamps, Time Zone, Authentication, Email Address
<span
v-if="errorsOrWarningsExist"
>- ERRORS / WARNINGS EXIST</span>
</h3>
<div v-if="expand">
<p
v-if="errorsOrWarningsExist"
class="errorMessage"
>
See error(s) below.
</p>
<div>
Account created:
<strong>{{ hero.auth.timestamps.created | formatDate }}</strong>
</div>
<div>
Most recent cron:
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
("auth.timestamps.loggedin")
</div>
<div v-if="cronError">
"lastCron" value:
<strong>{{ hero.lastCron | formatDate }}</strong>
<br>
<span class="errorMessage">
ERROR: cron probably crashed before finishing
("auth.timestamps.loggedin" and "lastCron" dates are different).
</span>
</div>
<div class="subsection-start">
Time zone:
<strong>{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
</div>
<div>
Custom Day Start time (CDS):
<strong>{{ hero.preferences.dayStart }}</strong>
</div>
<div v-if="timezoneDiffError || timezoneMissingError">
Time zone at previous cron:
<strong>{{ hero.preferences.timezoneOffsetAtLastCron | formatTimeZone }}</strong>
<div class="errorMessage">
<div v-if="timezoneDiffError">
ERROR: the player's current time zone is different than their time zone when
their previous cron ran. This can be because:
<ul>
<li>daylight savings started or stopped <sup>*</sup></li>
<li>the player changed zones due to travel <sup>*</sup></li>
<li>the player has devices set to different zones <sup>**</sup></li>
<li>the player uses a VPN with varying zones <sup>**</sup></li>
<li>something similarly unpleasant is happening. <sup>**</sup></li>
</ul>
<p>
<em>* The problem should fix itself in about a day.</em><br>
<em>** One of these causes is probably happening if the time zones stay
different for more than a day.</em>
</p>
</div>
<div v-if="timezoneMissingError">
ERROR: One of the player's time zones is missing.
This is expected and okay if it's the "Time zone at previous cron"
AND if it's their first day in Habitica.
Otherwise an error has occurred.
</div>
</div>
</div>
<div class="subsection-start form-inline">
API Token: &nbsp;
<form @submit.prevent="changeApiToken()">
<input
type="submit"
value="Change API Token"
class="btn btn-primary"
>
</form>
<div
v-if="tokenModified"
class="form-inline"
>
<strong>API Token has been changed. Tell the player something like this:</strong>
<br>
I've given you a new API Token.
You'll need to log out of the website and mobile app then log back in
otherwise they won't work correctly.
If you have trouble logging out, for the website go to
https://habitica.com/static/clear-browser-data and click the red button there,
and for the Android app, clear its data.
For the iOS app, if you can't log out you might need to uninstall it,
reboot your phone, then reinstall it.
</div>
</div>
<div class="subsection-start">
Local authentication:
<span v-if="hero.auth.local.email">Yes, &nbsp;
<strong>{{ hero.auth.local.email }}</strong></span>
<span v-else><strong>None</strong></span>
</div>
<div>
Google authentication:
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
<span v-else><strong>None</strong></span>
</div>
<div>
Facebook authentication:
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
<span v-else><strong>None</strong></span>
</div>
<div>
Apple ID authentication:
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
<span v-else><strong>None</strong></span>
</div>
<div class="subsection-start">
Full "auth" object for checking above is correct:
<pre>{{ hero.auth }}</pre>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment';
import formatDate from '../filters/formatDate';
import saveHero from '../mixins/saveHero';
function resetData (self) {
self.cronError = false;
self.timezoneDiffError = false;
self.timezoneMissingError = false;
self.errorsOrWarningsExist = false;
self.expand = false;
const cronDate1 = moment(self.hero.auth.timestamps.loggedin);
const cronDate2 = moment(self.hero.lastCron);
const maxAllowableSecondsDifference = 60; // expect cron to take less than this many seconds
if (Math.abs(cronDate1.diff(cronDate2, 'seconds')) > maxAllowableSecondsDifference) {
self.cronError = true;
self.errorsOrWarningsExist = true;
}
// compare the user's time zones to see if they're different
const newTimezone = self.hero.preferences.timezoneOffset;
const oldTimezone = self.hero.preferences.timezoneOffsetAtLastCron;
if ((newTimezone === undefined || oldTimezone === undefined)
&& (self.cronError || self.hero.flags.cronCount > 0)) {
self.timezoneMissingError = true;
self.errorsOrWarningsExist = true;
} else if (newTimezone !== oldTimezone) {
self.timezoneDiffError = true;
self.errorsOrWarningsExist = true;
}
self.expand = self.errorsOrWarningsExist;
}
export default {
filters: {
formatDate,
formatTimeZone (timezoneOffset) {
if (timezoneOffset === undefined) return 'No value recorded.';
// convert reverse offset to time zone in "+/-H:MM UTC" format
const sign = (timezoneOffset < 0) ? '+' : '-'; // reverse the sign
const timezoneHours = Math.floor(Math.abs(timezoneOffset) / 60);
const timezoneMinutes = Math.floor((Math.abs(timezoneOffset) / 60 - timezoneHours) * 60);
const timezoneMinutesDisplay = (timezoneMinutes) ? `:${timezoneMinutes}` : ''; // don't display :00
return `${sign}${timezoneHours}${timezoneMinutesDisplay} UTC`;
},
},
mixins: [
saveHero,
],
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
},
data () {
return {
cronError: false,
timezoneDiffError: false,
timezoneMissingError: false,
tokenModified: false,
errorsOrWarningsExist: false,
expand: false,
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
methods: {
authMethodExists (authMethod) {
if (this.hero.auth[authMethod] && this.hero.auth[authMethod].length !== 0) return true;
return false;
},
async changeApiToken () {
this.hero.changeApiToken = true;
await this.saveHero({ hero: this.hero, msg: 'API Token' });
this.tokenModified = true;
},
},
};
</script>

View File

@@ -0,0 +1,185 @@
<template>
<div v-if="hasPermission(user, 'userSupport')">
<div
v-if="hero && hero.profile"
class="row"
>
<div class="form col-12">
<basic-details
:user-id="hero._id"
:auth="hero.auth"
:preferences="hero.preferences"
:profile="hero.profile"
/>
<privileges-and-gems
:hero="hero"
:reset-counter="resetCounter"
/>
<cron-and-auth
:hero="hero"
:reset-counter="resetCounter"
/>
<party-and-quest
v-if="adminHasPrivForParty"
:user-id="hero._id"
:username="hero.auth.local.username"
:user-has-party="hasParty"
:party-not-exist-error="partyNotExistError"
:user-party-data="hero.party"
:group-party-data="party"
:reset-counter="resetCounter"
/>
<avatar-and-drops
:items="hero.items"
:preferences="hero.preferences"
/>
<items-owned
:hero="hero"
:reset-counter="resetCounter"
/>
<transactions
:hero="hero"
/>
<contributor-details
:hero="hero"
:reset-counter="resetCounter"
@clear-data="clearData"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
::v-deep .accordion-group .accordion-group {
margin-left: 1em;
}
::v-deep h3 {
margin-top: 2em;
}
::v-deep h4 {
margin-top: 1em;
}
::v-deep .expand-toggle::after {
margin-left: 5px;
}
::v-deep .subsection-start {
margin-top: 1em;
}
::v-deep .form-inline {
margin-bottom: 1em;
input, span {
margin-left: 5px;
}
}
::v-deep .errorMessage {
font-weight: bold;
}
::v-deep .markdownPreview {
margin-left: 3em;
margin-top: 1em;
}
</style>
<script>
import BasicDetails from './basicDetails';
import ItemsOwned from './itemsOwned';
import CronAndAuth from './cronAndAuth';
import PartyAndQuest from './partyAndQuest';
import AvatarAndDrops from './avatarAndDrops';
import PrivilegesAndGems from './privilegesAndGems';
import ContributorDetails from './contributorDetails';
import Transactions from './transactions';
import { userStateMixin } from '../../../mixins/userState';
export default {
components: {
BasicDetails,
ItemsOwned,
CronAndAuth,
PartyAndQuest,
AvatarAndDrops,
PrivilegesAndGems,
ContributorDetails,
Transactions,
},
mixins: [userStateMixin],
data () {
return {
userIdentifier: '',
resetCounter: 0,
hero: {},
party: {},
hasParty: false,
partyNotExistError: false,
adminHasPrivForParty: true,
};
},
watch: {
userIdentifier () {
// close modal if the page is opened in an existing tab from the modal
this.$root.$emit('bv::hide::modal', 'profile');
this.loadHero(this.userIdentifier);
},
},
mounted () {
this.userIdentifier = this.$route.params.userIdentifier;
},
methods: {
clearData () {
this.hero = {};
},
async loadHero (userIdentifier) {
const id = userIdentifier.replace(/@/, ''); // allow "@name" to be entered
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
if (!this.hero.flags) {
this.hero.flags = {
chatRevoked: false,
chatShadowMuted: false,
};
}
if (!this.hero.permissions) {
this.hero.permissions = {};
}
this.hasParty = false;
this.partyNotExistError = false;
this.adminHasPrivForParty = true;
if (this.hero.party && this.hero.party._id) {
try {
this.party = await this.$store.dispatch('hall:getHeroParty', { groupId: this.hero.party._id });
this.hasParty = true;
} catch (e) {
if (e.message.includes('status code 401')) {
// @TODO is there a better way to recognise NotAuthorized error?
this.adminHasPrivForParty = false;
} else {
// the API's error message isn't worth reporting ("Request failed with status code 404")
this.partyNotExistError = true;
}
}
}
this.resetCounter += 1; // tell child components to reinstantiate from scratch
},
},
beforeRouteUpdate (to, from, next) {
this.userIdentifier = to.params.userIdentifier;
next();
},
};
</script>

View File

@@ -0,0 +1,289 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Items
</h3>
<div v-if="expand">
<div>
The sections below display each item's key (bolded if the player has ever owned it),
followed by the item's English name.
<ul>
<li>
Click on an item's key or value to change it
(hovering shows an underline to show where you can click).
</li>
<li>For Mounts and Gear, clicking toggles between the allowed values.</li>
<li>For other item types, clicking gives you a form field to enter a new value.</li>
<li>Click Save when the correct value is displayed.</li>
<li>
You must Save for each item individually but you do not need to reload the user
between each Save.
</li>
<li>If you adjust an item and do not click Save for it, the change will be lost.</li>
</ul>
</div>
<div
v-for="itemType in itemTypes"
:key="itemType"
>
<div class="accordion-group">
<h4
class="expand-toggle"
:class="{'open': expandItemType[itemType]}"
@click="expandItemType[itemType] = !expandItemType[itemType]"
>
{{ itemType }}
</h4>
<div v-if="expandItemType[itemType]">
<p v-if="itemType === 'pets'">
A value of -1 means they owned the Pet but Released it
and have not yet rehatched it.
</p>
<p v-if="itemType === 'mounts'">
A value of "null" means they owned the Mount but Released it
and have not yet retamed it.
</p>
<p v-if="itemType === 'special'">
When there are 0 of these items, we can't tell if
they had been owned and were all used, or have never been owned.
</p>
<p v-if="itemType === 'gear'">
A value of true means they own the item now and can wear it.
A value of false means they used to own it but lost it from Death
(or an old Rebirth).
</p>
<ul>
<li
v-for="item in collatedItemData[itemType]"
:key="item.path"
>
<form @submit.prevent="saveItem(item)">
<span
class="enableValueChange"
@click="enableValueChange(item)"
>
{{ item | displayValue }}
:
<span :class="{ ownedItem: !item.neverOwned }">{{ item.key }} : </span>
</span>
<span v-html="item.name"></span>
<div
v-if="item.modified"
class="form-inline"
>
<input
v-if="item.valueIsInteger"
v-model="item.value"
class="form-control valueField"
type="number"
>
<input
v-if="item.modified"
type="submit"
value="Save"
class="btn btn-primary"
>
</div>
</form>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.ownedItem {
font-weight: bold;
}
.enableValueChange:hover {
text-decoration: underline;
}
.valueField {
min-width: 10ch;
}
</style>
<script>
import content from '@/../../common/script/content';
import getItemDescription from '../mixins/getItemDescription';
import saveHero from '../mixins/saveHero';
function collateItemData (self) {
const collatedItemData = {};
self.itemTypes.forEach(itemType => {
// itemTypes are pets, food, gear, etc
// Set up some basic data for this itemType:
let basePath = `items.${itemType}`;
let ownedItems = self.hero.items[itemType] || {};
let allItems = content[itemType];
if (itemType === 'gear') {
basePath = 'items.gear.owned';
ownedItems = self.hero.items.gear.owned || {};
allItems = content.gear.flat;
} else if (itemType === 'pets' || itemType === 'mounts') {
// add the non-Standard pets and mounts
const ucItemType = (itemType === 'pets') ? 'Pets' : 'Mounts';
self.petMountSubTypes.forEach(subType => {
allItems = { ...allItems, ...content[subType + ucItemType] };
});
}
const itemData = []; // all items for this itemType
// Collate data for items that the user owns or used to own:
for (const key of Object.keys(ownedItems)) {
// Do not sort keys. The order in the items object gives hints about order received.
if (itemType !== 'special' || self.specialItems.includes(key)) {
const valueIsInteger = !self.nonIntegerTypes.includes(itemType);
itemData.push({
neverOwned: false,
itemType,
key,
modified: false,
name: self.getItemDescription(itemType, key),
path: `${basePath}.${key}`,
value: ownedItems[key],
valueIsInteger,
});
}
}
// Collate data for items that the user never owned:
for (const key of Object.keys(allItems).sort()) {
if (
// ignore items the user owns because we captured them above:
!(key in ownedItems)
// ignore gear items that indicate empty equipped slots (e.g., head_base_0):
&& !(itemType === 'gear' && content.gear.flat[key].set
&& content.gear.flat[key].set === 'base-0')
// ignore "special" items that aren't Snowballs, Seafoam, etc:
&& (itemType !== 'special' || self.specialItems.includes(key))
) {
const valueIsInteger = !self.nonIntegerTypes.includes(itemType);
const value = (valueIsInteger) ? 0 : '';
itemData.push({
neverOwned: true,
itemType,
key,
modified: false,
name: self.getItemDescription(itemType, key),
path: `${basePath}.${key}`,
value,
valueIsInteger,
});
}
}
collatedItemData[itemType] = itemData;
});
return collatedItemData;
}
function resetData (self) {
self.collatedItemData = collateItemData(self);
self.itemTypes.forEach(itemType => { self.expandItemType[itemType] = false; });
}
export default {
filters: {
displayValue (item) {
if (item.value === '') return 'never owned';
if (item.value === 0 && item.neverOwned) return '0 (never owned)';
if (item.value === null) return 'null'; // we need visible text
return item.value; // true or false or an integer
},
},
mixins: [
getItemDescription,
saveHero,
],
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
expandItemType: {
eggs: false,
hatchingPotions: false,
food: false,
pets: false,
mounts: false,
quests: false,
gear: false,
special: false,
},
itemTypes: ['eggs', 'hatchingPotions', 'food', 'pets', 'mounts', 'quests', 'gear', 'special'],
nonIntegerTypes: ['mounts', 'gear'],
petMountSubTypes: ['premium', 'quest', 'special', 'wacky'], // e.g., 'premiumPets'
// items.special includes many things but we are interested in these only:
specialItems: ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'],
collatedItemData: {},
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
methods: {
async saveItem (item) {
// prepare the item's new value and path for being saved
this.hero.itemPath = item.path;
if (item.value === null) {
this.hero.itemVal = 'null';
} else if (item.value === false) {
this.hero.itemVal = 'false';
} else {
this.hero.itemVal = item.value;
}
await this.saveHero({ hero: this.hero, msg: item.key });
item.neverOwned = false;
item.modified = false;
},
enableValueChange (item) {
// allow form field(s) to be shown:
item.modified = true;
// for non-integer items, toggle through the allowed values:
if (item.itemType === 'gear') {
// Allowed starting values are true, false, and '' (never owned)
// Allowed values to switch to are true and false
item.value = !item.value;
} else if (item.itemType === 'mounts') {
// Allowed starting values are true, null, and "never owned"
// Allowed values to switch to are true and null
if (item.value === true) {
item.value = null;
} else {
item.value = true;
}
}
// @TODO add a delete option
},
},
};
</script>

View File

@@ -0,0 +1,317 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Party, Quest
<span
v-if="errorsOrWarningsExist"
>- ERRORS / WARNINGS EXIST</span>
</h3>
<div v-if="expand">
<div
v-if="errorsOrWarningsExist"
class="errorMessage"
>
<p v-if="partyNotExistError">
ERROR: User has a Party ID but that Party does not exist.
If you are seeing a red error notification on screen now
("<strong>Group with id ... not found</strong>"), it's refering to this issue.
<br>Ask a database admin to delete the user's Party ID ({{ userPartyData._id }}).
</p>
<p
v-if="questErrors"
v-html="questErrors"
></p>
</div>
<div>
Party:
<span v-if="userHasParty">
yes: party ID {{ groupPartyData._id }},
member count {{ groupPartyData.memberCount }} (may be wrong)
<br>
<span v-if="userIsPartyLeader">User is the party leader</span>
<span v-else>Party leader is
<router-link :to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}">
{{ groupPartyData.leader }}
</router-link>
</span>
</span>
<span v-else>no</span>
</div>
<div class="subsection-start">
<p v-html="questStatus"></p>
</div>
</div>
</div>
</template>
<script>
import * as quests from '@/../../common/script/content/quests';
function determineQuestStatus (self) {
// Quest data is in the user doc and party doc. They can be out of sync.
// Here we collate data from both sources, showing error messages if needed.
// First get data from the party's document.
const groupQuestData = self.groupPartyData.quest;
let questExists = false; // true if quest is active or in invitation stage
let questIsActive = false; // true if quest's invitation stage is over
let inviteStatusForUser = '';
let expectedRsvpStatusForUser = false;
let countOfQuestMembers = 0;
if (self.userHasParty && groupQuestData) {
questIsActive = groupQuestData.active;
if (groupQuestData.members) countOfQuestMembers = Object.keys(groupQuestData.members).length;
if (groupQuestData.key) {
questExists = true;
if (!countOfQuestMembers) {
self.questErrors = 'ERROR: Quest is running or in invitation stage but has no participants.';
} else if (groupQuestData.members[self.userId] === null) {
inviteStatusForUser = 'pending';
if (questIsActive) {
self.questErrors = 'ERROR: Quest is running but user\'s invitation is still pending ("null") in quest object.';
} else {
expectedRsvpStatusForUser = true;
}
} else if (groupQuestData.members[self.userId] === false) {
inviteStatusForUser = 'rejected';
if (questIsActive) {
self.questErrors = 'ERROR: Quest is running and user\'s invitation was rejected BUT '
+ 'it wasn\'t cleared properly from the quest\'s data ("false"). '
+ 'That shouldn\'t cause any problems though.';
}
} else if (groupQuestData.members[self.userId] === true) {
inviteStatusForUser = 'accepted';
} else if (questIsActive) {
inviteStatusForUser = 'rejected OR not accepted before quest start OR user joined party after quest started';
} else {
inviteStatusForUser = 'missing';
self.questErrors = 'ERROR: Quest is in invitation stage but user doesn\'t have an invitation '
+ 'in the party\'s data ("quest.members" needs to be fixed).';
}
} else if (questIsActive) {
self.questErrors = 'ERROR: Quest is running but there is no "key" to say which quest it is. '
+ 'This means the other data and errors in this section are unreliable, '
+ 'and there may be more errors not shown here.'
+ 'Other errors here may tell you which key to add.'
+ 'After fixing, check for more errors.';
// @TODO display a similar message for when it happens during invitation stage
}
}
if (self.questErrors) self.questErrors += '<br>';
// from this point on, further quest errors need to be appended to that
let questStatus = '<p>';
if (questExists) {
questStatus = 'Quest exists and is ';
if (questIsActive) {
questStatus += 'running.<br>User is ';
if (inviteStatusForUser !== 'accepted') questStatus += 'not ';
questStatus += 'a participant.';
} else {
questStatus += 'in invitation stage.<br>'
+ `User's invitation is ${inviteStatusForUser}.`;
}
questStatus += '<br>';
if (!groupQuestData.leader) {
self.questErrors += 'ERROR: quest does not have its owner specified '
+ '(party needs value for "quest.leader").<br>';
} else if (groupQuestData.leader === self.userId) {
questStatus += 'User is the quest owner.';
} else {
questStatus += `Quest owner is ${groupQuestData.leader}`;
}
} else {
questStatus = 'No quest.';
}
questStatus += '</p>';
// Assess quest participants.
if (questExists && countOfQuestMembers) {
const participants = (questIsActive) ? 'participants' : 'invitees';
questStatus += `<p>Quest has ${countOfQuestMembers} ${participants}:<ul>`;
for (const [memberId, inviteStatus] of Object.entries(groupQuestData.members)) {
questStatus += '<li>';
questStatus += (memberId === self.userId)
? `@${self.username}`
: memberId;
let invitationDescription = '';
const errMsg = ' - MINOR ERROR: this data should have been deleted when quest started';
if (inviteStatus === true) {
if (!questIsActive) invitationDescription = ' - invitation accepted';
// we don't display anything if quest is running - obvious that participant accepted
} else if (inviteStatus === false) {
invitationDescription += ' - invitation rejected';
if (questIsActive) invitationDescription += errMsg;
} else {
invitationDescription += ' - invitation pending';
if (questIsActive) invitationDescription += errMsg;
}
questStatus += invitationDescription;
questStatus += '</li>';
}
questStatus += '</ul></p>';
// @TODO: show error if all invitations accepted but quest not active
}
// Now get data from the user's document.
if (!self.userPartyData.quest) self.userPartyData.quest = {};
if (self.userPartyData.quest.RSVPNeeded !== expectedRsvpStatusForUser) {
self.questErrors
+= `ERROR: User's quest invitation ("party.quest.RSVPNeeded") should be "${expectedRsvpStatusForUser}" but isn't.<br>`;
}
if (inviteStatusForUser === 'pending' || inviteStatusForUser === 'accepted') {
if (!self.userPartyData.quest.key) {
self.questErrors += 'ERROR: User has accepted quest invitation or invitation is '
+ 'still pending but their account has no "key" for the quest.<br>';
} else if (self.userPartyData.quest.key !== groupQuestData.key) {
self.questErrors += 'ERROR: User has accepted quest invitation or invitation is '
+ `still pending but the "key" in their account (${self.userPartyData.quest.key}) `
+ `is different than the quest's "key" (${groupQuestData.key}).<br>`;
}
} else if (self.userPartyData.quest.key) {
self.questErrors += `ERROR: User has a "key" for the quest (${self.userPartyData.quest.key})`
+ 'but perhaps should not have (no quest exists, or user not participating, '
+ 'or quest is in erroneous state).<br>';
}
// Display details of quest (name, type, progress, etc).
if (questExists) {
const questContent = quests.quests[groupQuestData.key];
if (questContent) {
let questContentData = `<strong>Quest Details</strong>:<br>Quest name: ${questContent.text()}<br>Quest "key": ${questContent.key}`;
let questProgress = '<strong>Quest Progress:</strong>';
if (!questIsActive) questProgress += ' none (quest is in invitation stage)';
let userProgressToday;
let userMadeZeroProgress = false;
if (questContent.boss) {
// NB Data rounding below is done in the same way as on the user's party page.
questContentData += `<br>Boss name: ${questContent.boss.name()}`
+ `<br>Boss's starting HP: ${questContent.boss.hp}`
+ `<br>Boss's Strength: ${questContent.boss.str}`;
let bossHasRage;
if (questContent.boss.rage && questContent.boss.rage.value) {
bossHasRage = true;
questContentData += `<br>Boss's rage name for this quest: ${questContent.boss.rage.title()}`;
questContentData += `<br>Boss's rage limit: ${questContent.boss.rage.value}`;
}
if (questIsActive) {
if (!groupQuestData.progress || groupQuestData.progress.hp === undefined) {
self.questErrors += 'ERROR: Party\'s quest is missing some or all of the "progress" data.<br>';
} else {
questProgress += `<br>Current Boss HP: ${Math.ceil(groupQuestData.progress.hp * 100) / 100}`;
}
if (bossHasRage) {
questProgress += `<br>Current Rage: ${Math.floor(groupQuestData.progress.rage * 100) / 100}`;
}
}
userProgressToday = `Player's pending damage to Boss: ${Math.floor(self.userPartyData.quest.progress.up * 10) / 10}`;
if (!self.userPartyData.quest.progress.up) userMadeZeroProgress = true;
} else {
questContentData += '<br>Need to collect:<ul>';
if (questIsActive) questProgress += '<br>Current found items: <ul>';
for (const [key, obj] of Object.entries(questContent.collect)) {
questContentData += `<li>${obj.text()}: ${obj.count} ("key": ${key})</li>`;
if (questIsActive) {
if (!groupQuestData.progress || !groupQuestData.progress.collect) {
self.questErrors += 'ERROR: Party\'s quest is missing some or all of the "progress" data.<br>';
} else if (groupQuestData.progress.collect[key] !== undefined) {
questProgress += `<li>${obj.text()}: ${groupQuestData.progress.collect[key]}</li>`;
} else {
self.questErrors += `ERROR: Party's quest has no entry for "${key}" `
+ '("quest.progress.collect" needs to be fixed).<br>';
}
}
}
questContentData += '</ul>';
if (questIsActive) questProgress += '</ul>';
userProgressToday = `Player's pending collected items: ${self.userPartyData.quest.progress.collectedItems}`;
if (!self.userPartyData.quest.progress.collectedItems) userMadeZeroProgress = true;
}
if (userMadeZeroProgress) userProgressToday += '<br>NB: Zero pending quest progress may be from an error in which the user\'s database document is missing the pending progress fields. That error can\'t be identified here because the API will apply default data. If the user claims to have made pending progress but none is showing for them, a database admin has to check that.';
questStatus += `<p>${questContentData}</p>`
+ `<p>${questProgress}</p>`
+ `<p>${userProgressToday}</p>`;
questStatus += `<p><strong>Raw Quest Data:</strong></p><pre>party: ${JSON.stringify(groupQuestData, null, ' ')}`
+ `\nuser: ${JSON.stringify(self.userPartyData.quest, null, ' ')}</pre>`;
} else {
self.questErrors += `ERROR: quest "key" ${groupQuestData.key} does not match a known quest.`;
}
}
return questStatus;
}
function resetData (self) {
self.questStatus = '';
self.questErrors = '';
self.errorsOrWarningsExist = false;
self.expand = false;
if (self.partyNotExistError) {
self.errorsOrWarningsExist = true;
} else {
self.userIsPartyLeader = self.groupPartyData.leader === self.userId;
}
// check for quest errors even if party doesn't exist (user can have old quest data)
self.questStatus = determineQuestStatus(self);
if (self.questErrors) self.errorsOrWarningsExist = true;
self.expand = self.errorsOrWarningsExist;
}
export default {
props: {
resetCounter: {
type: Number,
required: true,
},
userId: {
type: String,
required: true,
},
username: {
type: String,
required: true,
},
userHasParty: {
type: Boolean,
required: true,
},
partyNotExistError: {
type: Boolean,
required: true,
},
userPartyData: {
type: Object,
required: true,
},
groupPartyData: {
type: Object,
required: true,
},
},
data () {
return {
userIsPartyLeader: false,
questStatus: '',
questErrors: '',
errorsOrWarningsExist: false,
expand: false,
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
};
</script>

View File

@@ -0,0 +1,143 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Privileges, Gem Balance
</h3>
<div v-if="expand">
<p
v-if="errorsOrWarningsExist"
class="errorMessage"
>
Player has had privileges removed or has moderation notes.
</p>
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
<div class="checkbox">
<label>
<input
v-if="hero.flags"
v-model="hero.flags.chatShadowMuted"
type="checkbox"
> Shadow Mute
</label>
</div>
<div class="checkbox">
<label>
<input
v-if="hero.flags"
v-model="hero.flags.chatRevoked"
type="checkbox"
> Mute (Revoke Chat Privileges)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.auth.blocked"
type="checkbox"
> Ban / Block
</label>
</div>
<div class="form-inline">
<label>
Balance
<input
v-model="hero.balance"
class="form-control balanceField"
type="number"
step="0.25"
>
</label>
<span>
<small>
Balance is in USD, not in Gems.
E.g., if this number is 1, it means 4 Gems.
Arrows change Balance by 0.25 (i.e., 1 Gem per click).
Do not use when awarding tiers; tier gems are automatic.
</small>
</span>
</div>
<div class="form-group">
<label>Moderation Notes</label>
<textarea
v-model="hero.secret.text"
class="form-control"
cols="5"
rows="5"
></textarea>
<div
v-markdown="hero.secret.text"
class="markdownPreview"
></div>
</div>
<input
type="submit"
value="Save"
class="btn btn-primary"
>
</form>
</div>
</div>
</template>
<style lang="scss" scoped>
.balanceField {
min-width: 15ch;
}
</style>
<script>
import markdownDirective from '@/directives/markdown';
import saveHero from '../mixins/saveHero';
function resetData (self) {
self.errorsOrWarningsExist = false;
self.expand = false;
if (self.hero.flags.chatRevoked || self.hero.flags.chatShadowMuted || self.hero.auth.blocked
|| (self.hero.secret.text && !self.hero.contributor.level)) {
// We automatically expand this section if the user has had privileges removed.
// We also expand if they have secret.text UNLESS they have a contributor tier because
// in that case the notes are probably about their contributions and can be seen in the
// Contributor Details section (which will be automatically expanded because of their tier).
self.errorsOrWarningsExist = true;
self.expand = true;
}
}
export default {
directives: {
markdown: markdownDirective,
},
mixins: [
saveHero,
],
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
},
data () {
return {
errorsOrWarningsExist: false,
expand: false,
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
};
</script>

View File

@@ -0,0 +1,52 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="toggleTransactionsOpen"
>
Transactions
</h3>
<div v-if="expand">
<purchase-history-table
:gem-transactions="gemTransactions"
:hourglass-transactions="hourglassTransactions"
/>
</div>
</div>
</template>
<script>
import PurchaseHistoryTable from '../../ui/purchaseHistoryTable.vue';
import { userStateMixin } from '../../../mixins/userState';
export default {
components: {
PurchaseHistoryTable,
},
mixins: [userStateMixin],
props: {
hero: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
gemTransactions: [],
hourglassTransactions: [],
};
},
methods: {
async toggleTransactionsOpen () {
this.expand = !this.expand;
if (this.expand) {
const transactions = await this.$store.dispatch('members:getPurchaseHistory', { memberId: this.hero._id });
this.gemTransactions = transactions.filter(transaction => transaction.currency === 'gems');
this.hourglassTransactions = transactions.filter(transaction => transaction.currency === 'hourglasses');
}
},
},
};
</script>

View File

@@ -84,8 +84,8 @@
</li>
<li v-if="user">
<a
@click.prevent="openBugReportModal()"
target="_blank"
@click.prevent="openBugReportModal()"
>
{{ $t('reportBug') }}
</a>
@@ -224,7 +224,7 @@
</div>
<div class="row">
<div class="col-12 col-md-5 text-center text-md-left">
© 2022 Habitica. All rights reserved.
© {{ currentYear }} Habitica. All rights reserved.
<div
v-if="!IS_PRODUCTION && isUserLoaded"
class="debug float-left"
@@ -512,6 +512,10 @@ export default {
if (!this.user) return null;
return `${base}?uuid=${this.user._id}`;
},
currentYear () {
const currentDate = new Date();
return currentDate.getFullYear();
},
},
methods: {
plusTenHealth () {
@@ -585,7 +589,7 @@ export default {
async makeAdmin () {
await axios.post('/api/v4/debug/make-admin');
// @TODO: Notification.text('You are now an admin!
// Go to the Hall of Heroes to change your contributor level.');
// Reload the website then go to Help > Admin Panel to set contributor level, etc.');
// @TODO: sync()
},
openModifyInventoryModal () {

View File

@@ -19,27 +19,6 @@
></div>
</div>
</div>
<div
class="form-group row text-center"
v-if="!registering"
>
<div class="col-12 col-md-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('facebook')"
>
<div
class="svg-icon social-icon"
v-html="icons.facebookIcon"
></div>
<div
class="text"
>
{{ $t('loginWithSocial', {social: 'Facebook'}) }}
</div>
</div>
</div>
</div>
<div class="form-group row text-center">
<div class="col-12 col-md-12">
<div
@@ -269,13 +248,13 @@
<label
v-once
for="usernameInput"
>{{ $t('email') }}</label>
>{{ $t('emailOrUsername') }}</label>
<input
id="usernameInput"
v-model="username"
class="form-control"
type="text"
:placeholder="$t('emailPlaceholder')"
:placeholder="$t('emailUsernamePlaceholder')"
>
</div>
<div class="text-center">

View File

@@ -79,7 +79,6 @@
></span>
<!-- Pet-->
<span
v-if="member.items.currentPet"
class="current-pet"
:class="petClass"
></span>
@@ -131,10 +130,12 @@
import some from 'lodash/some';
import moment from 'moment';
import { mapState } from '@/libs/store';
import foolPet from '../mixins/foolPet';
import ClassBadge from '@/components/members/classBadge';
export default {
mixins: [foolPet],
components: {
ClassBadge,
},
@@ -243,11 +244,12 @@ export default {
petClass () {
if (some(
this.currentEventList,
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'invert',
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'virtual',
)) {
return `Pet-${this.member.items.currentPet} invert`;
return this.foolPet(this.member.items.currentPet);
}
return `Pet-${this.member.items.currentPet}`;
if (this.member.items.currentPet) return `Pet-${this.member.items.currentPet}`;
return '';
},
},
methods: {

View File

@@ -125,6 +125,16 @@ export default {
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
background-color: $white;
.sprite.customize-option.shirt {
margin-left: -3px !important;
// otherwise its overriden by the .outer-option-background:not(.none) { rules
}
.sprite.customize-option.skin {
margin-left: -8px !important;
// otherwise its overriden by the .outer-option-background:not(.none) { rules
}
.option {
border: none;
border-radius: 2px;
@@ -203,17 +213,9 @@ export default {
.outer-option-background:not(.none) {
.sprite.customize-option {
// margin: 0 auto;
//margin-left: -3px;
//margin-top: -7px;
margin-top: 0;
margin-left: 0;
&.size, &.shirt {
margin-top: -8px;
margin-left: -4px;
}
&.color-bangs {
margin-top: 3px;
}

View File

@@ -12,12 +12,18 @@
{{ $t('reportBug') }}
</h2>
<div v-once class="report-bug-header-describe">
<div
v-once
class="report-bug-header-describe"
>
{{ $t('reportBugHeaderDescribe') }}
</div>
<div class="dialog-close">
<close-icon @click="close()" :purple="true"/>
<close-icon
:purple="true"
@click="close()"
/>
</div>
</div>
<div>
@@ -34,7 +40,10 @@
>
{{ $t('email') }}
</label>
<div class="mb-2 description-label" v-once>
<div
v-once
class="mb-2 description-label"
>
{{ $t('reportEmailText') }}
</div>
<input
@@ -47,7 +56,10 @@
:class="{'input-invalid': emailInvalid, 'input-valid': emailValid}"
>
<div class="error-label mt-2" v-if="emailInvalid">
<div
v-if="emailInvalid"
class="error-label mt-2"
>
{{ $t('reportEmailError') }}
</div>
</div>
@@ -55,7 +67,10 @@
<label v-once>
{{ $t('reportDescription') }}
</label>
<div class="mb-2 description-label" v-once>
<div
v-once
class="mb-2 description-label"
>
{{ $t('reportDescriptionText') }}
</div>
<textarea

View File

@@ -17,15 +17,22 @@
</div>
</div>
<div>
<span class="svg-icon check-icon"
v-html="icons.checkCircleIcon"
<span
class="svg-icon check-icon"
v-html="icons.checkCircleIcon"
></span>
<div class="title" v-once>
<div
v-once
class="title"
>
{{ $t('reportSent') }}
</div>
<div class="text mt-3 mb-4" v-once>
<div
v-once
class="text mt-3 mb-4"
>
{{ $t('reportSentDescription') }}
</div>
</div>
@@ -143,12 +150,12 @@ export default {
modalId: MODALS.BUG_REPORT_SUCCESS,
};
},
computed: {},
mounted () {},
methods: {
close () {
this.$root.$emit('bv::hide::modal', MODALS.BUG_REPORT_SUCCESS);
},
},
computed: {},
mounted () {},
};
</script>

Some files were not shown because too many files have changed in this diff Show More