Compare commits

..

20 Commits

Author SHA1 Message Date
SabreCat 0a22038d05 4.230.0 2022-05-10 13:56:00 -05: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
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
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
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
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
142 changed files with 3026 additions and 603 deletions
+1
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
+287 -99
View File
@@ -1,15 +1,16 @@
{
"name": "habitica",
"version": "4.229.1",
"version": "4.230.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@ampproject/remapping": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz",
"integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
"requires": {
"@jridgewell/trace-mapping": "^0.3.0"
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@babel/code-frame": {
@@ -26,20 +27,20 @@
"integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q=="
},
"@babel/core": {
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.9.tgz",
"integrity": "sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==",
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz",
"integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==",
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.17.9",
"@babel/helper-compilation-targets": "^7.17.7",
"@babel/generator": "^7.17.10",
"@babel/helper-compilation-targets": "^7.17.10",
"@babel/helper-module-transforms": "^7.17.7",
"@babel/helpers": "^7.17.9",
"@babel/parser": "^7.17.9",
"@babel/parser": "^7.17.10",
"@babel/template": "^7.16.7",
"@babel/traverse": "^7.17.9",
"@babel/types": "^7.17.0",
"@babel/traverse": "^7.17.10",
"@babel/types": "^7.17.10",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -56,28 +57,28 @@
}
},
"@babel/compat-data": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz",
"integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ=="
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz",
"integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw=="
},
"@babel/generator": {
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz",
"integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==",
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz",
"integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==",
"requires": {
"@babel/types": "^7.17.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
"@babel/types": "^7.17.10",
"@jridgewell/gen-mapping": "^0.1.0",
"jsesc": "^2.5.1"
}
},
"@babel/helper-compilation-targets": {
"version": "7.17.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz",
"integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==",
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz",
"integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==",
"requires": {
"@babel/compat-data": "^7.17.7",
"@babel/compat-data": "^7.17.10",
"@babel/helper-validator-option": "^7.16.7",
"browserslist": "^4.17.5",
"browserslist": "^4.20.2",
"semver": "^6.3.0"
}
},
@@ -129,36 +130,53 @@
}
},
"@babel/parser": {
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz",
"integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg=="
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz",
"integrity": "sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ=="
},
"@babel/traverse": {
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz",
"integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==",
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz",
"integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==",
"requires": {
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.17.9",
"@babel/generator": "^7.17.10",
"@babel/helper-environment-visitor": "^7.16.7",
"@babel/helper-function-name": "^7.17.9",
"@babel/helper-hoist-variables": "^7.16.7",
"@babel/helper-split-export-declaration": "^7.16.7",
"@babel/parser": "^7.17.9",
"@babel/types": "^7.17.0",
"@babel/parser": "^7.17.10",
"@babel/types": "^7.17.10",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz",
"integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==",
"requires": {
"@babel/helper-validator-identifier": "^7.16.7",
"to-fast-properties": "^2.0.0"
}
},
"browserslist": {
"version": "4.20.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz",
"integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==",
"requires": {
"caniuse-lite": "^1.0.30001332",
"electron-to-chromium": "^1.4.118",
"escalade": "^3.1.1",
"node-releases": "^2.0.3",
"picocolors": "^1.0.0"
}
},
"caniuse-lite": {
"version": "1.0.30001334",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001334.tgz",
"integrity": "sha512-kbaCEBRRVSoeNs74sCuq92MJyGrMtjWVfhltoHUCW4t4pXFvGjUBrfo47weBRViHkiV3eBYyIsfl956NtHGazw=="
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -169,10 +187,10 @@
"supports-color": "^5.3.0"
}
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
"node-releases": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz",
"integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ=="
},
"semver": {
"version": "6.3.0",
@@ -450,13 +468,13 @@
}
},
"@babel/generator": {
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz",
"integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==",
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz",
"integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==",
"requires": {
"@babel/types": "^7.17.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
"@babel/types": "^7.17.10",
"@jridgewell/gen-mapping": "^0.1.0",
"jsesc": "^2.5.1"
}
},
"@babel/helper-function-name": {
@@ -484,31 +502,31 @@
}
},
"@babel/parser": {
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz",
"integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg=="
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz",
"integrity": "sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ=="
},
"@babel/traverse": {
"version": "7.17.9",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz",
"integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==",
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz",
"integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==",
"requires": {
"@babel/code-frame": "^7.16.7",
"@babel/generator": "^7.17.9",
"@babel/generator": "^7.17.10",
"@babel/helper-environment-visitor": "^7.16.7",
"@babel/helper-function-name": "^7.17.9",
"@babel/helper-hoist-variables": "^7.16.7",
"@babel/helper-split-export-declaration": "^7.16.7",
"@babel/parser": "^7.17.9",
"@babel/types": "^7.17.0",
"@babel/parser": "^7.17.10",
"@babel/types": "^7.17.10",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
"version": "7.17.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz",
"integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==",
"requires": {
"@babel/helper-validator-identifier": "^7.16.7",
"to-fast-properties": "^2.0.0"
@@ -1432,10 +1450,24 @@
}
}
},
"@jridgewell/gen-mapping": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
"requires": {
"@jridgewell/set-array": "^1.0.0",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@jridgewell/resolve-uri": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz",
"integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew=="
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.6.tgz",
"integrity": "sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw=="
},
"@jridgewell/set-array": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.0.tgz",
"integrity": "sha512-SfJxIxNVYLTsKwzB3MoOQ1yxf4w/E6MdkvTgrgAt1bfxjSrLUoHMKrDOykwN14q65waezZIdqDneUIPh4/sKxg=="
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.11",
@@ -1443,9 +1475,9 @@
"integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg=="
},
"@jridgewell/trace-mapping": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz",
"integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==",
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -1675,9 +1707,9 @@
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="
},
"@types/body-parser": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz",
"integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==",
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
"requires": {
"@types/connect": "*",
"@types/node": "*"
@@ -1759,9 +1791,9 @@
}
},
"@types/express-serve-static-core": {
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz",
"integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==",
"version": "4.17.28",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz",
"integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==",
"requires": {
"@types/node": "*",
"@types/qs": "*",
@@ -1769,9 +1801,9 @@
}
},
"@types/express-unless": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.2.tgz",
"integrity": "sha512-Q74UyYRX/zIgl1HSp9tUX2PlG8glkVm+59r7aK4KGKzC5jqKIOX6rrVLRQrzpZUQ84VukHtRoeAuon2nIssHPQ==",
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz",
"integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==",
"requires": {
"@types/express": "*"
}
@@ -2347,6 +2379,19 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -5463,6 +5508,11 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"electron-to-chromium": {
"version": "1.4.129",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.129.tgz",
"integrity": "sha512-GgtN6bsDtHdtXJtlMYZWGB/uOyjZWjmRDumXTas7dGBaB9zUyCjzHet1DY2KhyHN8R0GLbzZWqm4efeddqqyRQ=="
},
"emitter-listener": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
@@ -6259,6 +6309,22 @@
"optional": true,
"requires": {
"glob": "^7.1.3"
},
"dependencies": {
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"optional": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"yallist": {
@@ -6813,6 +6879,22 @@
"requires": {
"glob": "^7.0.3",
"minimatch": "^3.0.3"
},
"dependencies": {
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"fill-range": {
@@ -6928,6 +7010,21 @@
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"requires": {
"glob": "^7.1.3"
},
"dependencies": {
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
}
}
@@ -7413,16 +7510,34 @@
"dev": true
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz",
"integrity": "sha512-cF7FYZZ47YzmCu7dDy50xSRRfO3ErRfrXuLZcNIuyiJEco0XSrGtuilG19L5xp3NcwTx7Gn+X6Tv3fmsUPTbow==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^5.0.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"requires": {
"balanced-match": "^1.0.0"
}
},
"minimatch": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
"requires": {
"brace-expansion": "^2.0.1"
}
}
}
},
"glob-parent": {
@@ -7450,6 +7565,19 @@
"unique-stream": "^2.0.2"
},
"dependencies": {
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
@@ -7744,6 +7872,19 @@
"slash": "^3.0.0"
},
"dependencies": {
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
@@ -9219,6 +9360,22 @@
"dev": true,
"requires": {
"glob": "^7.1.3"
},
"dependencies": {
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
}
}
@@ -9455,21 +9612,21 @@
}
},
"jwks-rsa": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz",
"integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.0.tgz",
"integrity": "sha512-GKOSDBWWBCiQTzawei6mEdRQvji5gecj8F9JwMt0ZOPnBPSmTjo5CKFvvbhE7jGPkU159Cpi0+OTLuABFcNOQQ==",
"requires": {
"@types/express-jwt": "0.0.42",
"debug": "^4.3.2",
"debug": "^4.3.4",
"jose": "^2.0.5",
"limiter": "^1.1.5",
"lru-memoizer": "^2.1.4"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
@@ -10534,6 +10691,22 @@
"dev": true,
"requires": {
"glob": "^7.1.3"
},
"dependencies": {
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
}
}
@@ -12138,9 +12311,9 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"rate-limiter-flexible": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.6.tgz",
"integrity": "sha512-8DVFOe89rreyut/vzwBI7vgXJynyYoYnH5XogtAKs0F/neAbCTTglXxSJ7fZeZamcFXZDvMidCBvps4KM+1srw=="
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.7.tgz",
"integrity": "sha512-dmc+J/IffVBvHlqq5/XClsdLdkOdQV/tjrz00cwneHUbEDYVrf4aUDAyR4Jybcf2+Vpn4NwoVrnnAyt/D0ciWw=="
},
"raw-body": {
"version": "2.5.1",
@@ -12708,6 +12881,21 @@
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
},
"dependencies": {
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"run-async": {
@@ -13806,21 +13994,21 @@
"integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ=="
},
"superagent": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.2.tgz",
"integrity": "sha512-o9/fP6dww7a4xmEF5a484o2rG34UUGo8ztDlv7vbCWuqPhpndMi0f7eXxdlryk5U12Kzy46nh8eNpLAJ93Alsg==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.3.tgz",
"integrity": "sha512-WA6et4nAvgBCS73lJvv1D0ssI5uk5Gh+TGN/kNe+B608EtcVs/yzfl+OLXTzDs7tOBDIpvgh/WUs1K2OK1zTeQ==",
"requires": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.3",
"debug": "^4.3.3",
"debug": "^4.3.4",
"fast-safe-stringify": "^2.1.1",
"form-data": "^4.0.0",
"formidable": "^2.0.1",
"methods": "^1.1.2",
"mime": "^2.5.0",
"qs": "^6.10.1",
"qs": "^6.10.3",
"readable-stream": "^3.6.0",
"semver": "^7.3.5"
"semver": "^7.3.7"
},
"dependencies": {
"debug": {
@@ -13840,9 +14028,9 @@
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"requires": {
"lru-cache": "^6.0.0"
}
+6 -6
View File
@@ -1,10 +1,10 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.229.1",
"version": "4.230.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.17.9",
"@babel/core": "^7.17.10",
"@babel/preset-env": "^7.16.11",
"@babel/register": "^7.17.7",
"@google-cloud/trace-agent": "^5.1.6",
@@ -30,7 +30,7 @@
"express": "^4.17.3",
"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,7 +43,7 @@
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^4.0.2",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.0.5",
"jwks-rsa": "^2.1.0",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
@@ -61,14 +61,14 @@
"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",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.0",
"stripe": "^8.219.0",
"superagent": "^7.1.2",
"superagent": "^7.1.3",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^8.3.2",
+13 -10
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);
});
});
@@ -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;
+3 -3
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: {
+1 -1
View File
@@ -877,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);
});
@@ -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, {
@@ -231,7 +231,7 @@ describe('GET challenges/user', () => {
publicGuild = group;
await user.update({
'contributor.admin': true,
'permissions.challengeAdmin': true,
});
officialChallenge = await generateChallenge(user, group, {
@@ -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,
},
});
@@ -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', () => {
@@ -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());
@@ -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;
@@ -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');
@@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => {
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'contributor.sudo': true,
'permissions.coupons': true,
});
});
@@ -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');
@@ -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');
@@ -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 () => {
@@ -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 });
@@ -315,7 +315,7 @@ describe('GET /groups/:id', () => {
beforeEach(async () => {
admin = await generateUser({
'contributor.admin': true,
'permissions.moderator': true,
});
});
@@ -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();
});
});
});
@@ -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', () => {
@@ -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);
});
});
@@ -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']);
});
@@ -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
});
});
@@ -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;
});
});
@@ -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 });
@@ -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}`);
@@ -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',
@@ -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({
@@ -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 },
@@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => {
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'contributor.sudo': true,
'permissions.coupons': true,
});
});
@@ -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'),
});
});
+3 -3
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'),
});
});
+1 -1
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([
+1 -1
View File
@@ -15,7 +15,7 @@ describe('GET /news/:newsID', () => {
};
beforeEach(async () => {
user = await generateUser({
'contributor.newsPoster': true,
'permissions.news': true,
});
});
+3 -3
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.',
});
});
+3 -3
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.',
});
});
+1 -1
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({
+1
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 },
@@ -675,6 +675,11 @@
width: 141px;
height: 147px;
}
.background_castle_gate {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_castle_gate.png');
width: 141px;
height: 147px;
}
.background_champions_colosseum {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_champions_colosseum.png');
width: 141px;
@@ -860,6 +865,11 @@
width: 141px;
height: 147px;
}
.background_enchanted_music_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_enchanted_music_room.png');
width: 141px;
height: 147px;
}
.background_fairy_ring {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_fairy_ring.png');
width: 141px;
@@ -1389,6 +1399,11 @@
width: 141px;
height: 147px;
}
.background_on_a_castle_wall {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_castle_wall.png');
width: 141px;
height: 147px;
}
.background_on_tree_branch {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_tree_branch.png');
width: 141px;
@@ -2171,6 +2186,11 @@
width: 68px;
height: 68px;
}
.icon_background_castle_gate {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_castle_gate.png');
width: 68px;
height: 68px;
}
.icon_background_champions_colosseum {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_champions_colosseum.png');
width: 68px;
@@ -2361,6 +2381,11 @@
width: 68px;
height: 68px;
}
.icon_background_enchanted_music_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_enchanted_music_room.png');
width: 68px;
height: 68px;
}
.icon_background_fairy_ring {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_fairy_ring.png');
width: 68px;
@@ -2890,6 +2915,11 @@
width: 68px;
height: 68px;
}
.icon_background_on_a_castle_wall {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_on_a_castle_wall.png');
width: 68px;
height: 68px;
}
.icon_background_on_tree_branch {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_on_tree_branch.png');
width: 68px;
@@ -19055,6 +19085,11 @@
width: 114px;
height: 90px;
}
.shield_armoire_snareDrum {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_snareDrum.png');
width: 114px;
height: 90px;
}
.shield_armoire_softBlackPillow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softBlackPillow.png');
width: 114px;
@@ -19085,6 +19120,11 @@
width: 114px;
height: 90px;
}
.shield_armoire_spanishGuitar {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_spanishGuitar.png');
width: 114px;
height: 90px;
}
.shield_armoire_strawberryFood {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_strawberryFood.png');
width: 90px;
@@ -20240,6 +20280,11 @@
width: 68px;
height: 68px;
}
.shop_shield_armoire_snareDrum {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_snareDrum.png');
width: 68px;
height: 68px;
}
.shop_shield_armoire_softBlackPillow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_softBlackPillow.png');
width: 68px;
@@ -20270,6 +20315,11 @@
width: 68px;
height: 68px;
}
.shop_shield_armoire_spanishGuitar {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_spanishGuitar.png');
width: 68px;
height: 68px;
}
.shop_shield_armoire_strawberryFood {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_strawberryFood.png');
width: 68px;
@@ -20485,6 +20535,11 @@
width: 68px;
height: 68px;
}
.shop_weapon_armoire_huntingHorn {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_huntingHorn.png');
width: 68px;
height: 68px;
}
.shop_weapon_armoire_ironCrook {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_ironCrook.png');
width: 68px;
@@ -21315,6 +21370,11 @@
width: 90px;
height: 90px;
}
.weapon_armoire_huntingHorn {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_huntingHorn.png');
width: 114px;
height: 90px;
}
.weapon_armoire_ironCrook {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_ironCrook.png');
width: 90px;
@@ -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`;
}
@@ -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>
@@ -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?';
},
},
};
@@ -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' });
}
},
},
};
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
+1 -1
View File
@@ -589,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 () {
@@ -321,7 +321,7 @@ import cloneDeep from 'lodash/cloneDeep';
import omit from 'lodash/omit';
import { v4 as uuid } from 'uuid';
import { mapState } from '@/libs/store';
import { userStateMixin } from '../../mixins/userState';
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
import closeChallengeModal from './closeChallengeModal';
import Column from '../tasks/column';
@@ -358,7 +358,7 @@ export default {
userLink,
groupLink,
},
mixins: [challengeMemberSearchMixin],
mixins: [challengeMemberSearchMixin, userStateMixin],
props: ['challengeId'],
data () {
return {
@@ -387,7 +387,6 @@ export default {
};
},
computed: {
...mapState({ user: 'user.data' }),
isMember () {
return this.user.challenges.indexOf(this.challenge._id) !== -1;
},
@@ -396,7 +395,7 @@ export default {
return this.user._id === this.challenge.leader._id;
},
isAdmin () {
return Boolean(this.user.contributor.admin);
return this.hasPermission(this.user, 'challengeAdmin');
},
canJoin () {
return !this.isMember;
@@ -112,7 +112,7 @@
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="group in categoryOptions"
v-if="group.key !== 'habitica_official' || user.contributor.admin"
v-if="group.key !== 'habitica_official' || hasPermission(user, 'challengeAdmin')"
:key="group.key"
class="form-check"
>
@@ -277,14 +277,15 @@ import clone from 'lodash/clone';
import throttle from 'lodash/throttle';
import markdownDirective from '@/directives/markdown';
import { userStateMixin } from '../../mixins/userState';
import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '@/../../common/script/constants';
import { mapState } from '@/libs/store';
export default {
directives: {
markdown: markdownDirective,
},
mixins: [userStateMixin],
props: ['groupId'],
data () {
const categoryOptions = [
@@ -378,7 +379,6 @@ export default {
};
},
computed: {
...mapState({ user: 'user.data' }),
creating () {
return !this.workingChallenge.id;
},
@@ -5,7 +5,7 @@
class="mentioned-icon"
></div>
<div
v-if="user.contributor.admin && msg.flagCount"
v-if="hasPermission(user, 'moderator') && msg.flagCount"
class="message-hidden"
>
{{ flagCountDescription }}
@@ -54,7 +54,7 @@
</div>
<div
v-if="(user.flags.communityGuidelinesAccepted && msg.uuid !== 'system')
&& (!isMessageReported || user.contributor.admin)"
&& (!isMessageReported || hasPermission(user, 'moderator'))"
class="action d-flex align-items-center"
@click="report(msg)"
>
@@ -68,7 +68,7 @@
</div>
</div>
<div
v-if="msg.uuid === user._id || user.contributor.admin"
v-if="msg.uuid === user._id || hasPermission(user, 'moderator')"
class="action d-flex align-items-center"
@click="remove()"
>
@@ -202,7 +202,7 @@ import cloneDeep from 'lodash/cloneDeep';
import escapeRegExp from 'lodash/escapeRegExp';
import renderWithMentions from '@/libs/renderWithMentions';
import { mapState } from '@/libs/store';
import { userStateMixin } from '../../mixins/userState';
import userLink from '../userLink';
import deleteIcon from '@/assets/svg/delete.svg';
@@ -223,6 +223,7 @@ export default {
return moment(value).toDate().toString();
},
},
mixins: [userStateMixin],
props: {
msg: {},
groupId: {},
@@ -240,7 +241,6 @@ export default {
};
},
computed: {
...mapState({ user: 'user.data' }),
isUserMentioned () {
const message = this.msg;
@@ -149,7 +149,7 @@ import moment from 'moment';
import axios from 'axios';
import debounce from 'lodash/debounce';
import findIndex from 'lodash/findIndex';
import { mapState } from '@/libs/store';
import { userStateMixin } from '../../mixins/userState';
import Avatar from '../avatar';
import copyAsTodoModal from './copyAsTodoModal';
@@ -161,6 +161,7 @@ export default {
chatCard,
Avatar,
},
mixins: [userStateMixin],
props: {
chat: {},
groupType: {},
@@ -182,7 +183,6 @@ export default {
};
},
computed: {
...mapState({ user: 'user.data' }),
// @TODO: We need a different lazy load mechnism.
// But honestly, adding a paging route to chat would solve this
messages () {
@@ -214,7 +214,7 @@ export default {
canViewFlag (message) {
if (message.uuid === this.user._id) return true;
if (!message.flagCount || message.flagCount < 2) return true;
return this.user.contributor.admin;
return this.hasPermission(this.user, 'moderator');
},
loadProfileCache: debounce(function loadProfileCache (screenPosition) {
this._loadProfileCache(screenPosition);
@@ -23,7 +23,7 @@
</div>
<div class="footer text-center">
<button
v-if="user.contributor.admin"
v-if="hasPermission(user, 'moderator')"
class="pull-left btn btn-danger"
@click="clearFlagCount()"
>
@@ -88,15 +88,15 @@
</style>
<script>
import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import markdownDirective from '@/directives/markdown';
import { userStateMixin } from '../../mixins/userState';
export default {
directives: {
markdown: markdownDirective,
},
mixins: [notifications],
mixins: [notifications, userStateMixin],
data () {
const abuseFlagModalBody = {
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
@@ -111,9 +111,6 @@ export default {
reportComment: '',
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.$root.$on('habitica::report-chat', this.handleReport);
},
@@ -288,7 +288,7 @@
import extend from 'lodash/extend';
import groupUtilities from '@/mixins/groupsUtilities';
import styleHelper from '@/mixins/styleHelper';
import { mapState, mapGetters } from '@/libs/store';
import { mapGetters } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import participantListModal from './participantListModal';
import groupFormModal from './groupFormModal';
@@ -312,6 +312,7 @@ import QuestDetailModal from './questDetailModal';
import RightSidebar from '@/components/groups/rightSidebar';
import InvitationListModal from './invitationListModal';
import { PAGES } from '@/libs/consts';
import { userStateMixin } from '../../mixins/userState';
export default {
components: {
@@ -327,7 +328,7 @@ export default {
directives: {
markdown: markdownDirective,
},
mixins: [groupUtilities, styleHelper],
mixins: [groupUtilities, styleHelper, userStateMixin],
props: ['groupId'],
data () {
return {
@@ -356,9 +357,6 @@ export default {
};
},
computed: {
...mapState({
user: 'user.data',
}),
...mapGetters({
partyMembers: 'party:members',
}),
@@ -372,7 +370,7 @@ export default {
return this.user._id === this.group.leader._id;
},
isAdmin () {
return Boolean(this.user.contributor.admin);
return Boolean(this.hasPermission(this.user, 'moderator'));
},
isMember () {
return this.isMemberOfGroup(this.user, this.group);
@@ -213,7 +213,7 @@ label.custom-control-label(v-once) {{ $t('allowGuildInvitationsFromNonMembers')
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="group in categoryOptions"
v-if="group.key !== 'habitica_official' || user.contributor.admin"
v-if="group.key !== 'habitica_official' || hasPermission(user, 'challengeAdmin')"
:key="group.key"
class="form-check"
>
@@ -372,13 +372,13 @@ label.custom-control-label(v-once) {{ $t('allowGuildInvitationsFromNonMembers')
</style>
<script>
import { mapState } from '@/libs/store';
import toggleSwitch from '@/components/ui/toggleSwitch';
import markdownDirective from '@/directives/markdown';
import gemIcon from '@/assets/svg/gem.svg';
import informationIcon from '@/assets/svg/information.svg';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '@/../../common/script/constants';
import { userStateMixin } from '../../mixins/userState';
// @TODO: Not sure the best way to pass party creating status
// Since we need the modal in the header, passing props doesn't work
@@ -393,6 +393,7 @@ export default {
directives: {
markdown: markdownDirective,
},
mixins: [userStateMixin],
data () {
const data = {
workingGroup: {
@@ -491,7 +492,6 @@ export default {
return data;
},
computed: {
...mapState({ user: 'user.data' }),
editingGroup () {
return this.$store.state.editingGroup;
},
@@ -512,7 +512,7 @@ export default {
return this.workingGroup.type === 'party';
},
isAdmin () {
return Boolean(this.user.contributor.admin);
return Boolean(this.hasPermission(this.user, 'moderator'));
},
},
watch: {
@@ -379,7 +379,6 @@
<script>
import orderBy from 'lodash/orderBy';
import isEmpty from 'lodash/isEmpty';
import { mapState } from '@/libs/store';
import removeMemberModal from '@/components/members/removeMemberModal';
import loadingGryphon from '@/components/ui/loadingGryphon';
@@ -390,6 +389,7 @@ import starIcon from '@/assets/members/star.svg';
import dots from '@/assets/svg/dots.svg';
import SelectList from '@/components/ui/selectList';
import { PAGES } from '@/libs/consts';
import { userStateMixin } from '../../mixins/userState';
export default {
components: {
@@ -398,6 +398,7 @@ export default {
removeMemberModal,
loadingGryphon,
},
mixins: [userStateMixin],
props: ['hideBadge'],
data () {
return {
@@ -462,13 +463,12 @@ export default {
};
},
computed: {
...mapState({ user: 'user.data' }),
isLeader () {
if (!this.group || !this.group.leader) return false;
return this.user._id === this.group.leader || this.user._id === this.group.leader._id;
},
isAdmin () {
return Boolean(this.user.contributor.admin);
return Boolean(this.hasPermission(this.user, 'moderator'));
},
isLoadMoreAvailable () {
// Only available if the current length of `members` is less than the
+16 -27
View File
@@ -8,7 +8,7 @@
</div>
<div class="row standard-page">
<div>
<div v-if="user.contributor.admin">
<div v-if="hasPermission(user, 'userSupport')">
<h2>Reward User</h2>
<div
v-if="!hero.profile"
@@ -247,9 +247,6 @@
<thead>
<tr>
<th>{{ $t('name') }}</th>
<th v-if="user.contributor && user.contributor.admin">
{{ $t('userId') }}
</th>
<th>{{ $t('contribLevel') }}</th>
<th>{{ $t('title') }}</th>
<th>{{ $t('contributions') }}</th>
@@ -257,12 +254,12 @@
</thead>
<tbody>
<tr
v-for="(hero, index) in heroes"
v-for="hero in heroes"
:key="hero._id"
>
<td>
<user-link
v-if="hero.contributor && hero.contributor.admin"
v-if="hasPermission(hero, 'userSupport')"
:user="hero"
:popover="$t('gamemaster')"
popover-trigger="mouseenter"
@@ -272,13 +269,17 @@
v-else
:user="hero"
/>
</td>
<td
v-if="user.contributor.admin"
class="btn-link"
@click="populateContributorInput(hero._id, index)"
>
{{ hero._id }}
<span v-if="hasPermission(user, 'userSupport')">
<br>
{{ hero._id }}
<br>
<router-link
:to="{ name: 'adminPanelUser',
params: { userIdentifier: hero._id } }"
>
admin panel
</router-link>
</span>
</td>
<td>{{ hero.contributor.level }}</td>
<td>{{ hero.contributor.text }}</td>
@@ -305,10 +306,8 @@
<script>
import each from 'lodash/each';
import markdownDirective from '@/directives/markdown';
import styleHelper from '@/mixins/styleHelper';
import { mapState } from '@/libs/store';
import * as quests from '@/../../common/script/content/quests';
import { mountInfo, petInfo } from '@/../../common/script/content/stable';
import content from '@/../../common/script/content';
@@ -316,6 +315,7 @@ import gear from '@/../../common/script/content/gear';
import notifications from '@/mixins/notifications';
import userLink from '../userLink';
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue';
import { userStateMixin } from '../../mixins/userState';
export default {
components: {
@@ -325,7 +325,7 @@ export default {
directives: {
markdown: markdownDirective,
},
mixins: [notifications, styleHelper],
mixins: [notifications, styleHelper, userStateMixin],
data () {
return {
heroes: [],
@@ -347,9 +347,6 @@ export default {
expandTransactions: false,
};
},
computed: {
...mapState({ user: 'user.data' }),
},
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('hallContributors'),
@@ -392,11 +389,9 @@ export default {
},
getFormattedItemReference (pathPrefix, itemKeys, values) {
let finishedString = '\n'.concat('path: ', pathPrefix, ', ', 'value: {', values, '}\n');
each(itemKeys, key => {
finishedString = finishedString.concat('\t', pathPrefix, '.', key, '\n');
});
return finishedString;
},
async loadHero (uuid, heroIndex) {
@@ -413,7 +408,6 @@ export default {
this.expandAuth = false;
},
async saveHero () {
this.hero.contributor.admin = this.hero.contributor.level > 7;
const heroUpdated = await this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
this.text('User updated');
this.hero = {};
@@ -426,11 +420,6 @@ export default {
this.heroID = -1;
this.currentHeroIndex = -1;
},
populateContributorInput (id, index) {
this.heroID = id;
window.scrollTo(0, 200);
this.loadHero(id, index);
},
async toggleTransactionsOpen () {
this.expandTransactions = !this.expandTransactions;
if (this.expandTransactions) {
@@ -9,7 +9,7 @@
<thead>
<tr>
<th>{{ $t('name') }}</th>
<th v-if="user.contributor.admin">
<th v-if="hasPermission(user, 'userSupport')">
{{ $t('userId') }}
</th>
<th>{{ $t('backerTier') }}</th>
@@ -28,7 +28,7 @@
></a>
{{ patron.profile.name }}
</td>
<td v-if="user.contributor.admin">
<td v-if="hasPermission(user, 'userSupport')">
{{ patron._id }}
</td>
<td>{{ patron.backer.tier }}</td>
@@ -40,19 +40,16 @@
</template>
<script>
import { mapState } from '@/libs/store';
import styleHelper from '@/mixins/styleHelper';
import { userStateMixin } from '../../mixins/userState';
export default {
mixins: [styleHelper],
mixins: [styleHelper, userStateMixin],
data () {
return {
patrons: [],
};
},
computed: {
...mapState({ user: 'user.data' }),
},
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('hallPatrons'),
@@ -297,6 +297,14 @@
{{ $t('help') }}
</router-link>
<div class="topbar-dropdown">
<router-link
v-if="user.permissions.fullAccess ||
user.permissions.userSupport || user.permissions.newsPoster"
class="topbar-dropdown-item dropdown-item"
:to="{name: 'adminPanel'}"
>
Admin Panel
</router-link>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'faq'}"
@@ -118,6 +118,7 @@ import { MAX_LEVEL_HARD_CAP } from '@/../../common/script/constants';
import notifications from '@/mixins/notifications';
import guide from '@/mixins/guide';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
import yesterdailyModal from './tasks/yesterdailyModal';
import newStuff from './news/modal';
@@ -841,11 +842,21 @@ export default {
},
async runCronAction () {
// Run Cron
await axios.post('/api/v4/cron');
// Reset daily analytics actions
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
const response = await axios.post('/api/v4/cron');
if (response.status === 200) {
// Reset daily analytics actions
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
} else {
// Note a failed cron event, for our records and investigation
Analytics.track({
eventName: 'cron failed',
eventAction: 'cron failed',
eventCategory: 'behavior',
hitType: 'event',
responseCode: response.status,
}, { trackOnClient: true });
}
// Sync
await Promise.all([
@@ -38,7 +38,7 @@
{{ $t('subscription') }}
</router-link>
<router-link
v-if="user.contributor.admin"
v-if="hasPermission(user, 'userSupport')"
class="nav-link"
:to="{name: 'transactions'}"
:class="{'active': $route.name === 'transactions'}"
@@ -123,11 +123,13 @@ import find from 'lodash/find';
import { mapState } from '@/libs/store';
import SecondaryMenu from '@/components/secondaryMenu';
import gifts from '@/assets/svg/gifts-vertical.svg';
import { userStateMixin } from '../../mixins/userState';
export default {
components: {
SecondaryMenu,
},
mixins: [userStateMixin],
data () {
return {
icons: Object.freeze({
@@ -138,7 +140,6 @@ export default {
computed: {
...mapState({
currentEventList: 'worldState.data.currentEventList',
user: 'user.data',
}),
currentEvent () {
return find(this.currentEventList, event => Boolean(event.promo));
@@ -22,7 +22,7 @@
<div>
<small>{{ $t('couponText') }}</small>
</div>
<div v-if="user.contributor.sudo">
<div v-if="user.permissions.coupons">
<hr>
<h4>{{ $t('generateCodes') }}</h4>
<div
@@ -143,12 +143,11 @@ export default {
},
methods: {
close () {
this.validateInputs();
this.$root.$emit('bv::hide::modal', 'restore');
},
restore () {
if (this.restoreValues.stats.lvl < 1) {
// @TODO:
// Notification.error(env.t('invalidLevel'), true);
if (!this.validateInputs()) {
return;
}
@@ -175,6 +174,35 @@ export default {
this.$store.dispatch('user:set', settings);
this.$root.$emit('bv::hide::modal', 'restore');
},
validateInputs () {
const canRestore = ['hp', 'exp', 'gp', 'mp'];
let valid = true;
for (const stat of canRestore) {
if (this.restoreValues.stats[stat] === '') {
this.restoreValues.stats[stat] = this.user.stats[stat];
valid = false;
}
}
const inputLevel = Number(this.restoreValues.stats.lvl);
if (this.restoreValues.stats.lvl === ''
|| !Number.isInteger(inputLevel)
|| inputLevel < 1) {
this.restoreValues.stats.lvl = this.user.stats.lvl;
valid = false;
}
const inputStreak = Number(this.restoreValues.achievements.streak);
if (this.restoreValues.achievements.streak === ''
|| !Number.isInteger(inputStreak)
|| inputStreak < 0) {
this.restoreValues.achievements.streak = this.user.achievements.streak;
valid = false;
}
return valid;
},
},
};
</script>
@@ -59,7 +59,7 @@
></div>
</button>
<button
v-if="userLoggedIn.contributor.admin"
v-if="hasPermission(userLoggedIn, 'moderator')"
v-b-tooltip.hover.right="'Admin - Toggle Tools'"
class="btn btn-secondary positive-icon d-flex justify-content-center align-items-center"
@click="toggleAdminTools()"
@@ -71,7 +71,7 @@
</button>
</div>
<div
v-if="userLoggedIn.contributor.admin && adminToolsLoaded"
v-if="hasPermission(userLoggedIn, 'moderator') && adminToolsLoaded"
class="row admin-profile-actions"
>
<div class="col-12 text-right">
@@ -111,6 +111,12 @@
class="admin-action"
@click="adminUnblockUser()"
>un-ban</span>
<router-link
:to="{ name: 'adminPanelUser', params: { userIdentifier: userId } }"
replace
>
Admin Panel
</router-link>
</div>
</div>
<div class="row">
@@ -730,6 +736,7 @@ import challenge from '@/assets/svg/challenge.svg';
import member from '@/assets/svg/member-icon.svg';
import staff from '@/assets/svg/tier-staff.svg';
import error404 from '../404';
import { userCustomStateMixin } from '../../mixins/userState';
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
@@ -742,6 +749,7 @@ export default {
profileStats,
error404,
},
mixins: [userCustomStateMixin('userLoggedIn')],
props: ['userId', 'startingPage'],
data () {
return {
@@ -780,7 +788,6 @@ export default {
},
computed: {
...mapState({
userLoggedIn: 'user.data',
flatGear: 'content.gear.flat',
}),
userJoinedDate () {
+17 -4
View File
@@ -1,7 +1,20 @@
import { mapState } from '@/libs/store';
export const userStateMixin = { // eslint-disable-line import/prefer-default-export
computed: {
...mapState({ user: 'user.data' }),
},
export const userCustomStateMixin = fieldname => {
const map = { };
map[fieldname] = 'user.data';
return { // eslint-disable-line import/prefer-default-export
computed: {
...mapState(map),
},
methods: {
hasPermission (user, permission) {
return Boolean((user.permissions
&& (user.permissions[permission] || user.permissions.fullAccess))
|| (user.contributor && user.contributor.admin));
},
},
};
};
export const userStateMixin = userCustomStateMixin('user');
@@ -800,7 +800,7 @@ export default {
await this.reload();
// close members modal if the Private Messages page is opened in an existing tab
// close modal if the Private Messages page is opened in an existing tab
this.$root.$emit('bv::hide::modal', 'profile');
this.$root.$emit('bv::hide::modal', 'members-modal');
+45 -4
View File
@@ -6,7 +6,7 @@ import handleRedirect from './handleRedirect';
import ParentPage from '@/components/parentPage';
import { PAGES } from '@/libs/consts';
// NOTE: when adding a page make sure to implement setTitle
// NOTE: when adding a page make sure to implement the `common:setTitle` action
// Static Pages
const StaticWrapper = () => import(/* webpackChunkName: "entry" */'@/components/static/staticWrapper');
@@ -53,6 +53,10 @@ const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/i
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
// Admin Panel
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel');
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/user-support');
// Except for tasks that are always loaded all the other main level
// All the main level
// components are loaded in separate webpack chunks.
@@ -109,7 +113,7 @@ const router = new VueRouter({
scrollBehavior () {
return { x: 0, y: 0 };
},
// requiresLogin is true by default, isStatic false
// meta defaults: requiresLogin true, privilegeNeeded empty
// NOTE: when adding a new route entry make sure to implement the `common:setTitle` action
// in the route component to set a specific subtitle for the page.
routes: [
@@ -348,6 +352,31 @@ const router = new VueRouter({
{ name: 'contributors', path: 'contributors', component: HeroesPage },
],
},
{
name: 'adminPanel',
path: '/admin-panel',
component: AdminPanelPage,
meta: {
privilegeNeeded: [ // any one of these is enough to give access
'userSupport',
'newsPoster',
],
},
children: [
{
name: 'adminPanelUser',
path: ':userIdentifier', // User ID or Username
component: AdminPanelUserPage,
meta: {
privilegeNeeded: [
'userSupport',
],
},
},
],
},
// Only used to handle some redirects
// See router.beforeEach
{ path: '/redirect/:redirect', name: 'redirect' },
@@ -357,9 +386,10 @@ const router = new VueRouter({
const store = getStore();
router.beforeEach((to, from, next) => {
const { isUserLoggedIn } = store.state;
router.beforeEach(async (to, from, next) => {
const { isUserLoggedIn, isUserLoaded } = store.state;
const routeRequiresLogin = to.meta.requiresLogin !== false;
const routePrivilegeNeeded = to.meta.privilegeNeeded;
if (to.name === 'redirect') return handleRedirect(to, from, next);
@@ -392,6 +422,17 @@ router.beforeEach((to, from, next) => {
return next({ name: 'tasks' });
}
if (routePrivilegeNeeded) {
// Redirect non-admin users when trying to access a page.
if (!isUserLoaded) await store.dispatch('user:fetch');
if (!store.state.user.data.permissions.fullAccess) {
const userHasPriv = routePrivilegeNeeded.some(
privName => store.state.user.data.permissions[privName],
);
if (!userHasPriv) return next({ name: 'tasks' });
}
}
// Redirect old guild urls
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
const splits = to.hash.split('/');
+6
View File
@@ -26,3 +26,9 @@ export async function getPatrons (store, payload) {
const response = await axios.get(url);
return response.data.data;
}
export async function getHeroParty (store, payload) {
const url = `/api/v4/hall/heroes/party/${payload.groupId}`;
const response = await axios.get(url);
return response.data.data;
}
+2 -2
View File
@@ -46,7 +46,7 @@ export function canDelete (store) {
const user = store.state.user.data;
const userId = user.id || user._id;
const isUserAdmin = user.contributor && !!user.contributor.admin;
const isUserAdmin = user.permissions && user.permissions.challengeAdmin;
const isUserGroupLeader = group && (group.leader
&& group.leader._id === userId);
const isUserGroupManager = group && (group.managers
@@ -84,7 +84,7 @@ export function canEdit (store) {
const user = store.state.user.data;
const userId = user.id || user._id;
const isUserAdmin = user.contributor && !!user.contributor.admin;
const isUserAdmin = user.permissions && user.permissions.challengeAdmin;
const isUserGroupLeader = group && (group.leader
&& group.leader._id === userId);
const isUserGroupManager = group && (group.managers
@@ -39,7 +39,7 @@ describe('canDelete getter', () => {
});
it('can Delete any challenge task as admin', () => {
store.state.user.data.contributor.admin = true;
store.state.user.data.permissions = { challengeAdmin: true };
expect(store.getters['tasks:canDelete'](task, 'challenge', true, null, challenge)).to.equal(true);
});
@@ -39,7 +39,7 @@ describe('canEdit getter', () => {
});
it('can Edit any challenge task if admin', () => {
store.state.user.data.contributor.admin = true;
store.state.user.data.permissions = { challengeAdmin: true };
expect(store.getters['tasks:canEdit'](task, 'challenge', true, null, challenge)).to.equal(true);
expect(store.getters['tasks:canEdit'](task, 'challenge', false, null, challenge)).to.equal(true);
+3 -1
View File
@@ -690,5 +690,7 @@
"backgrounds042022": "SET 95: Veröffentlicht im April 2022",
"backgroundBlossomingTreesNotes": "Verweile unter blühenden Bäumen.",
"backgroundFlowerShopText": "Blumenladen",
"backgroundFlowerShopNotes": "Genieße den süßen Duft eines Blumenladens."
"backgroundFlowerShopNotes": "Genieße den süßen Duft eines Blumenladens.",
"backgroundSpringtimeLakeText": "Frühlingssee",
"backgroundSpringtimeLakeNotes": "Genieße die Aussicht an den Ufern eines Frühlingssees."
}
+2 -1
View File
@@ -370,5 +370,6 @@
"hatchingPotionSunset": "Sonnenuntergang",
"hatchingPotionSolarSystem": "Sonnensystem",
"hatchingPotionMoonglow": "Mondschein",
"hatchingPotionOnyx": "Onyx"
"hatchingPotionOnyx": "Onyx",
"hatchingPotionVirtualPet": "Virtuelles Haustier"
}
+1 -1
View File
@@ -13,7 +13,7 @@
"companyDonate": "Spenden",
"forgotPassword": "Passwort vergessen?",
"emailNewPass": "Einen Link per E-Mail senden, um das Passwort zurückzusetzen",
"forgotPasswordSteps": "Trage deinen Benutzernamen oder die E-Mail-Adresse ein, mit der Du Deinen Habitica-Account aktiviert hast.",
"forgotPasswordSteps": "Trage Deinen Benutzernamen oder die E-Mail-Adresse ein, mit der Du Deinen Habitica-Account aktiviert hast.",
"sendLink": "Link senden",
"featuredIn": "Vorgestellt in",
"footerDevs": "Entwickler",
+14 -5
View File
@@ -393,7 +393,7 @@
"questTaskwoodsTerror3DropStrawberry": "Erdbeere (Futter)",
"questTaskwoodsTerror3DropWeapon": "Laterne des Aufgabenwaldes (Zweihändige Waffe)",
"questFerretText": "Das ruchlose Frettchen",
"questFerretNotes": "Während Du durch Habit City spazierst, siehst Du wie eine unzufriedene Menge sich um ein rot-gekleidetes Frettchen schart. \"Der Produktivitäts-Trank ist nutzlos!\" beschwert sich @Beffymaroo. \"Ich habe gestern abend drei Stunden ferngesehen, anstatt meine Aufgaben zu erledigen.\"\"Genau!\" schreit @Pandah. \"Und heute habe ich eine Stunde damit verbracht meine Bücher zu lesen, anstatt sie zu lesen!\" <br>Das ruchlose Frettchen hebt unschuldig die Hände. \"Das ist mehr fernsehen und Bücher sortieren als Du normalerweise machen würdest, oder nicht?\" <br><br>Die Menge bricht in Zorn aus. <br> \"Kein Rückgeld!\" krächtst das ruchlose Frettchen. Er schleudert einen magischen Blitz in die Menge und bereitet sich darauf vor in dem Rauch zu verschwinden. <br><br>\"Bitte, Habiticaner!\" sagt @Faye, während sie Dich am Arm fässt. \"Besiege das Frettchen und zwinge es dazu das Geld aus seinen unehrlichen Machenschaften zurück zu geben!\"",
"questFerretNotes": "Während Du durch Habit City spazierst, siehst Du wie eine unzufriedene Menge sich um ein rot-gekleidetes Frettchen schart. \"Der Produktivitäts-Trank ist nutzlos!\" beschwert sich @Beffymaroo. \"Ich habe gestern abend drei Stunden ferngesehen, anstatt meine Aufgaben zu erledigen.\"\"Genau!\" schreit @Pandah. \"Und heute habe ich eine Stunde damit verbracht meine Bücher zu lesen, anstatt sie zu lesen!\" <br>Das ruchlose Frettchen hebt unschuldig die Hände. \"Das ist mehr fernsehen und Bücher sortieren als Du normalerweise machen würdest, oder nicht?\" <br><br>Die Menge bricht in Zorn aus. <br> \"Kein Rückgeld!\" krächtst das ruchlose Frettchen. Er schleudert einen magischen Blitz in die Menge und bereitet sich darauf vor in dem Rauch zu verschwinden. <br><br>\"Bitte, Habiticaner!\" sagt @Faye, während sie Dich am Arm fässt. \"Besiege das Frettchen und zwinge es dazu das Geld aus seinen unehrlichen Machenschaften zurück zu geben!\"",
"questFerretCompletion": "Du schlägst den weichpelzigen Betrüger in die Flucht und @UncommonCriminal erstattet der Menge die Rückzahlung. Es ist sogar etwas Gold für Dich übrig und es sieht so aus, als hätte das Ruchlose Frettchen in der Eile zu entkommen einige Eier fallen lassen!",
"questFerretBoss": "Ruchloses Frettchen",
"questFerretDropFerretEgg": "Frettchen (Ei)",
@@ -433,7 +433,7 @@
"questGroupStoikalmCalamity": "Stoïstilles Unglück",
"questStoikalmCalamity1Text": "Stoïstilles Unglück, Teil 1: Erdgegner",
"questStoikalmCalamity1Notes": "Ein knappes Schreiben von @Kiwibot trifft ein; nicht nur ist die frostbedeckte Schriftrolle eiskalt, sondern sie lässt Dir auch kalte Schauer den Rücken runterlaufen. \"Bin in Stoïstillen Steppen Monster platzen aus Boden brauche Hilfe!\" Du versammelst Deine Gruppe und reitest gen Norden, doch gerade, als Ihr Euch den Berg hinabbewegt, explodiert der Schnee unter Euren Füßen und grausig grinsende Schädel umzingeln Euch! <br><br>Plötzlich fliegt ein Speer an Euch vorbei und gräbt sich in einen Schädel, der Dich, sich durch den Schnee buddelnd, unbemerkt angreifen wollte. Eine große Frau in fein geschmiedeter Rüstung galoppiert auf dem Rücken eines Mastodons in die Schlacht und zieht mit wehendem Zopf rabiat den Speer wieder aus dem zerquetschten Biest. Zeit, die Feinde mit der Hilfe von Lady Glaciate, der Anführerin der Mammutreiter, zu bekämpfen!",
"questStoikalmCalamity1Completion": "Als Du den letzten Schädeln den Gnadenstoß versetzt, lösen sie sich in einen Hauch Magie auf. \"Der verflixte Schwarm mag zwar verschwunden sein\", sagt Lady Glaciate, \"aber wir haben größere Probleme. Folge mir.\" Sie wirft Dir zum Schutz vor der eisigen Luft einen Mantel zu und Du reitest ihr nach.",
"questStoikalmCalamity1Completion": "Als Du den letzten Schädeln den Gnadenstoß versetzt, lösen sie sich in einen Hauch Magie auf. \"Der verflixte Schwarm mag zwar verschwunden sein\", sagt Lady Glaciate, \"aber wir haben größere Probleme. Folge mir.\" Sie wirft Dir zum Schutz vor der eisigen Luft einen Mantel zu und Du reitest ihr nach.",
"questStoikalmCalamity1Boss": "Erdschädelschwarm",
"questStoikalmCalamity1RageTitle": "Schwarmnachwuchs",
"questStoikalmCalamity1RageDescription": "Schwarmnachwuchs: Dieser Balken füllt sich, wenn Du Deine Tagesaufgaben nicht erfüllst. Wenn er voll ist, heilt sich der Erdschädelschwarm um 30% seiner verbleibenden Lebenspunkte!",
@@ -525,7 +525,7 @@
"questLostMasterclasser1CollectForbiddenTomes": "Verbotene Bücher",
"questLostMasterclasser1CollectHiddenTomes": "Versteckte Bücher",
"questLostMasterclasser2Text": "Das Geheimnis der Klassenmeister, Teil 2: Beschwörung des v'Schwinders",
"questLostMasterclasser2Notes": "Der Fröhliche Reaper trommelt mit ihren knochigen Fingern auf den Büchern, die ihr mitgebracht habt. “Ach je”, sagt der Meister der Heiler. “Da ist eine bösartige Lebensessenz am Werk. Ich hätte es mir denken können, wenn man die Angriffe der wiederbelebten Schädel während der Vorfälle berücksichtigt.” Ihre rechte Hand @tricksy.fox bringt eine Truhe herein, und Du bist überrascht zu sehen, was beffymaroo daraus hervorholt: es sind genau die Gegenstände, die einst von der mysteriösen Tzina benutzt wurden, um anderen ihren Willen aufzuzwingen.<br><br>“Ich werde mit resonierender Heilmagie versuchen, die Kreatur zu manifestieren”, sagt der Fröhliche Reaper, und erinnert Dich daran, dass das Skelett ein eher unkonventioneller Heiler ist. “Du musst die enthüllten Informationen schnell lesen, für den Fall dass sie freikommt.” <br><br>Als sie sich konzentriert, fließt wirbelnder Nebel aus den Büchern und windet sich um die Gegenstände. Du blätterst schnell durch die Seiten, in dem Versuch, die neuen Textzeilen zu lesen, die wabernd wieder sichtbar werden. Du kannst nur ein paar Bruchstücke erfassen: “Sand der Zeitwüste” — “die Große Katastrophe” —“in vier Teile gespalten”— “für immer verdorben”— bevor Dir ein einzelner Name ins Auge springt: Zinnya. <br><br> Schlagartig befreien sich die Seiten aus Deinen Händen und zerfallen in der Luft in tausend Schnipsel, als mit einer Explosion eine heulende Kreatur erscheint und sich mit den Gegenständen verbindet. <br><br>“Das ist ein v'Schwinder!” ruft der Fröhliche Reaper und wirft einen Schutzzauber über euch. “Das sind alte Kreaturen der Verwirrung und Verschleierung. Wenn diese Tzina so einen kontrollieren kann, muss sie eine beängstigende Macht über Lebensmagie haben. Schnell, greift ihn an, bevor er wieder in die Bücher flüchtet!”<br><br>",
"questLostMasterclasser2Notes": "Der Fröhliche Reaper trommelt mit ihren knochigen Fingern auf den Büchern, die ihr mitgebracht habt. “Ach je”, sagt der Meister der Heiler. “Da ist eine bösartige Lebensessenz am Werk. Ich hätte es mir denken können, wenn man die Angriffe der wiederbelebten Schädel während der Vorfälle berücksichtigt.” Ihre rechte Hand @tricksy.fox bringt eine Truhe herein, und Du bist überrascht zu sehen, was beffymaroo daraus hervorholt: es sind genau die Gegenstände, die einst von der mysteriösen Tzina benutzt wurden, um anderen ihren Willen aufzuzwingen.<br><br>“Ich werde mit resonierender Heilmagie versuchen, die Kreatur zu manifestieren”, sagt der Fröhliche Reaper, und erinnert Dich daran, dass das Skelett ein eher unkonventioneller Heiler ist. “Du musst die enthüllten Informationen schnell lesen, für den Fall dass sie freikommt.” <br><br>Als sie sich konzentriert, fließt wirbelnder Nebel aus den Büchern und windet sich um die Gegenstände. Du blätterst schnell durch die Seiten, in dem Versuch, die neuen Textzeilen zu lesen, die wabernd wieder sichtbar werden. Du kannst nur ein paar Bruchstücke erfassen: “Sand der Zeitwüste” — “die Große Katastrophe” —“in vier Teile gespalten”— “für immer verdorben”— bevor Dir ein einzelner Name ins Auge springt: Zinnya. <br><br> Schlagartig befreien sich die Seiten aus Deinen Händen und zerfallen in der Luft in tausend Schnipsel, als mit einer Explosion eine heulende Kreatur erscheint und sich mit den Gegenständen verbindet. <br><br>“Das ist ein v'Schwinder!” ruft der Fröhliche Reaper und wirft einen Schutzzauber über euch. “Das sind alte Kreaturen der Verwirrung und Verschleierung. Wenn diese Tzina so einen kontrollieren kann, muss sie eine beängstigende Macht über Lebensmagie haben. Schnell, greift ihn an, bevor er wieder in die Bücher flüchtet!”<br><br>",
"questLostMasterclasser2Completion": "Der v'Schwinder unterliegt endlich, und Du liest die Schnipsel vor.<br><br>“Keine dieser Referenzen klingt vertraut, auch nicht für jemanden, der so alt ist wie ich”, sagt der Fröhliche Reaper. “Außer.... die Zeitwüste ist eine entfernte Wüste am unwirtlichsten Rand von Habitica. Portale versagen oft in der Nähe, aber schnelle Reittiere könnten Dich im Handumdrehen dorthin bringen. Lady Glaciate wird gerne helfen.” Ihre Stimme wird immer amüsierter. \"Das bedeutet, dass der verliebte Meister der Schurken zweifellos mitkommen wird.\" Sie gibt dir die schimmernde Maske. \"Vielleicht solltest du versuchen, die verbleibende Magie in diesen Gegenständen bis zur Quelle zu verfolgen. Ich werde etwas Nahrung für Deine Reise sammeln.\"",
"questLostMasterclasser2Boss": "Der v'Schwinder",
"questLostMasterclasser2DropEyewear": "Äthermaske (Brille)",
@@ -574,7 +574,7 @@
"questBadgerUnlockText": "Schaltet den Kauf von Dachseiern auf dem Marktplatz frei",
"questDysheartenerText": "Der Entmutiger",
"questDysheartenerNotes": "Es ist Valentinstag, die Sonne geht gerade auf, als plötzlich ein erschütternder Krach die Luft zerreißt. Ein kränkliches rosa Licht durchdringt die Gebäude, und Ziegel zerbrechen, als sich ein tiefer Riss auf Habit City's Hauptstraße auftut. Ein überirdisches Kreischen ertönt durch die Luft und lässt die Fenster zerspringen, während sich eine ungeschlachte Form aus der klaffenden Erde erhebt.<br><br>Mandibeln schnappen, der Panzer glitzert; Bein um Bein entfaltet sich in der Luft. Die Menge beginnt zu schreien, als die insektoide Kreatur aufsteht und sich als schrecklichste aller Kreaturen zu erkennen gibt: der furchterregende Entmutiger höchstselbst. Er heult erwartungsvoll und stürzt vor, um an den Hoffnungen hart arbeitender Habiticaner zu nagen. Mit jedem scharrenden Kratzen seiner stacheligen Vorderbeine fühlst Du, wie sich Dein Herz in der Brust vor Verzweiflung weiter zusammenzieht.<br><br>“Fasst Euch alle ein Herz!” ruft Lemoness. “Er denkt vielleicht, dass wir leichte Ziele sind, weil so viele von uns entmutigende Neujahrsvorsätze haben, aber er wird feststellen, dass Habiticaner wissen, wie man an seinen Zielen festhält!”<br><br>AnnDeLune hebt ihren Stab. “Lasst uns unsere Aufgaben angehen und dieses Monster erledigen!”",
"questDysheartenerCompletion": "<strong>Der Entmutiger wurde BESIEGT!</strong><br><br>Gemeinsam holen alle in Habitica zu einem letzten Schlag mit ihren Aufgaben aus, und der Entmutiger weicht mit einem bestürzten Kreischen zurück. “Stimmt etwas nicht, Entmutiger?” ruft AnnDeLune mit funkelnden Augen. “Fühlst Du Dich entmutigt?”<br><br>Glühend pinke Risse zeigen sich auf dem Panzer des Entmutigers, und er zerbricht mit einer verpuffenden rosa Rauchwolke. Eine Flut von köstlichen Süßigkeiten regnet auf alle hernieder, während sich im ganzen Land ein erneuertes Gefühl von Kraft und Entschlossenheit ausbreitet.<br><br>Die Menge jubelt wild, Umarmungen werden ausgetauscht, und die Haustiere kauen glücklich auf ihren verspäteten Valentinsleckerli. Plötzlich liegt Gesang in der Luft, ein fröhlicher Chor ertönt, und funkelnde Silhouetten ziehen über den Himmel.<br><br>Unser neu-gewonnener Optimismus hat eine Herde Hippogreifen angelockt! Die anmutigen Kreaturen setzen auf dem Boden auf, sträuben interessiert ihre Federn und tänzeln auf der Stelle. “Wie es aussieht, haben wir neue Freunde gefunden, die uns helfen, nicht zu verzagen, selbst wenn unsere Aufgaben noch so beängstigend sind”, sagt Lemoness. <br><br>Beffymaroo hat bereits ihre Arme voll mit gefiederten Plüschbällen. “Vielleicht helfen sie uns, die zerstörten Gebiete Habitica's wieder aufzubauen!”<br><br>Summend und singend ziehen die Hippogreifen voran, während alle Habiticaner zusammenarbeiten, um unsere geliebte Heimat wieder aufzubauen.",
"questDysheartenerCompletion": "<strong>Der Entmutiger wurde BESIEGT!</strong><br><br>Gemeinsam holen alle in Habitica zu einem letzten Schlag mit ihren Aufgaben aus, und der Entmutiger weicht mit einem bestürzten Kreischen zurück. “Stimmt etwas nicht, Entmutiger?” ruft AnnDeLune mit funkelnden Augen. “Fühlst Du Dich entmutigt?”<br><br>Glühend pinke Risse zeigen sich auf dem Panzer des Entmutigers, und er zerbricht mit einer verpuffenden rosa Rauchwolke. Eine Flut von köstlichen Süßigkeiten regnet auf alle hernieder, während sich im ganzen Land ein erneuertes Gefühl von Kraft und Entschlossenheit ausbreitet.<br><br>Die Menge jubelt wild, Umarmungen werden ausgetauscht, und die Haustiere kauen glücklich auf ihren verspäteten Valentinsleckerli. Plötzlich liegt Gesang in der Luft, ein fröhlicher Chor ertönt, und funkelnde Silhouetten ziehen über den Himmel.<br><br>Unser neu-gewonnener Optimismus hat eine Herde Hippogreifen angelockt! Die anmutigen Kreaturen setzen auf dem Boden auf, sträuben interessiert ihre Federn und tänzeln auf der Stelle. “Wie es aussieht, haben wir neue Freunde gefunden, die uns helfen, nicht zu verzagen, selbst wenn unsere Aufgaben noch so beängstigend sind”, sagt Lemoness. <br><br>Beffymaroo hat bereits ihre Arme voll mit gefiederten Plüschbällen. “Vielleicht helfen sie uns, die zerstörten Gebiete Habitica's wieder aufzubauen!”<br><br>Summend und singend ziehen die Hippogreifen voran, während alle Habiticaner zusammenarbeiten, um unsere geliebte Heimat wieder aufzubauen.",
"questDysheartenerCompletionChat": "`Der Entmutiger wurde BESIEGT!'`\n\nGemeinsam holen alle in Habitica zu einem letzten Schlag mit ihren Aufgaben aus, und der Entmutiger weicht mit einem bestürzten Kreischen zurück. “Stimmt etwas nicht, Entmutiger?” ruft AnnDeLune mit funkelnden Augen. “Fühlst Du Dich entmutigt?”\n\nGlühend pinke Risse zeigen sich auf dem Panzer des Entmutigers, und er zerbricht mit einer verpuffenden rosa Rauchwolke. Eine Flut von köstlichen Süßigkeiten regnet auf alle hernieder, während sich im ganzen Land ein erneuertes Gefühl von Kraft und Entschlossenheit ausbreitet.\n\nDie Menge jubelt wild, Umarmungen werden ausgetauscht, und die Haustiere kauen glücklich auf ihren verspäteten Valentinsleckerli. Plötzlich liegt Gesang in der Luft, ein fröhlicher Chor ertönt, und funkelnde Silhouetten ziehen über den Himmel.\n\nUnser neu-gewonnener Optimismus hat eine Herde Hippogreifen angelockt! Die anmutigen Kreaturen setzen auf dem Boden auf, sträuben interessiert ihre Federn und tänzeln auf der Stelle. “Wie es aussieht, haben wir neue Freunde gefunden, die uns helfen, nicht zu verzagen, selbst wenn unsere Aufgaben noch so beängstigend sind”, sagt Lemoness.\n\nBeffymaroo hat bereits ihre Arme voll mit gefiederten Plüschbällen. “Vielleicht helfen sie uns, die zerstörten Gebiete Habitica's wieder aufzubauen!”\n\nSummend und singend ziehen die Hippogreifen voran, während alle Habiticaner zusammenarbeiten, um unsere geliebte Heimat wieder aufzubauen.",
"questDysheartenerBossRageTitle": "Niederschmetternder Herzschmerz",
"questDysheartenerBossRageDescription": "Die Anzeige für den Raserei-Angriff füllt sich, wenn Habiticaner ihre Tagesaufgaben nicht abhaken. Sobald sie gefüllt ist, wird der Entmutiger seine Niederschmetternde Herzschmerz-Attacke über einem von Habitica's Ladenbesitzern entfesseln, also strengt Euch an und erledigt Eure Aufgaben!",
@@ -744,5 +744,14 @@
"questOnyxCollectLeoRunes": "Leo Runen",
"questOnyxCollectOnyxStones": "Onyx Steine",
"questOnyxDropOnyxPotion": "Onyx Schlüpfelixier",
"questOnyxUnlockText": "Schaltet das Onyx Schlüpfelixier zum Kauf auf dem Marktplatz frei"
"questOnyxUnlockText": "Schaltet das Onyx Schlüpfelixier zum Kauf auf dem Marktplatz frei",
"questVirtualPetBoss": "Wotchimon",
"questVirtualPetRageTitle": "Das Piepen",
"questVirtualPetRageDescription": "Dieser Balken füllt sich, wenn Du Deine Tagesaufgaben nicht abschließt. Ist er vollständig gefüllt, wird Wotchimon sich um 30% seiner verbleibenden Gesundheit heilen!",
"questVirtualPetRageEffect": "`Wotchimon setzt lästiges Piepen ein!` Wotchimon lässt ein lästiges Piepen ertönen, und seine Zufriedenheitsanzeige verschwindet plötzlich! Ausstehender Schaden reduziert.",
"questVirtualPetDropVirtualPetPotion": "Virtuelles Haustier Schlüpfelixier",
"questVirtualPetUnlockText": "Schaltet das Virtuelles Haustier Schlüpfelixier zum Kauf auf dem Marktplatz frei",
"questVirtualPetText": "Virtuelles Chaos mit dem April-Scherzkeks: Das Piepen",
"questVirtualPetCompletion": "Vorsichtiges Betätigen von Knöpfen scheint die mysteriösen Bedürfnisse des virtuellen Haustiers erfüllt zu haben, und es hat sich endlich beruhigt und wirkt zufrieden.<br><br>Plötzlich erscheint in einem Konfettiregen der April-Scherzkeks mit einem Korb voller seltsamer Elixiere, die leise vor sich hin piepen.<br><br>\"Gutes Timing, April-Scherzkeks,\" sagt @Beffymaroo mit einem schiefen lächeln. \"Ich vermute, dieser große Kerl ist ein Bekannter von Dir.\"<br><br>\"Hm, ja,\" sagt der April-Scherzkeks verlegen. \"Es tut mir sehr leid, und ich danke euch beiden dafür, dass ihr euch um Wotchimon gekümmert habt! Nehmt diese Elixiere als Dank, sie können eure virtuellen Haustiere jederzeit zurückbringen!\"<br><br>Du bist dir noch nicht zu 100% sicher, ob Du mit dem vielen Piepen einverstanden bist, aber sie sind so süß, dass es einen Versuch wert ist!",
"questVirtualPetNotes": "Es ist ein schöner, ruhiger Frühlingsmorgen in Habitica, eine Woche nach einem erinnerungswürdigen ersten April. Du und @Beffymaroo seid in den Ställen und kümmert euch um eure Haustiere (welche immer noch ein wenig verwirrt sind von der Zeit, die sie als virtuelle Haustiere verbracht haben!).<br><br>In der Ferne hört ihr ein Grollen und ein Piepen, zunächst leise, aber schnell an Lautstärke gewinnend, als käme es näher. Eine Ei-Form erscheint am Horizont und während sie sich nähert und noch lauter piept erkennt ihr, dass es ein gigantisches virtuelles Haustier ist!<br><br>\"Oh nein\" ruft @Beffymaroo, \"Ich fürchte der April-Scherzkeks hat mit diesem großen Kerl noch ein paar unerledigte Angelegenheiten, er scheint Aufmerksamkeit zu wollen!\"<br><br>Das virtuelle Haustier piept wütend, bekommt einen virtuellen Wutanfall und nähert sich immer weiter."
}
@@ -779,6 +779,14 @@
"backgroundSpringtimeLakeText": "Springtime Lake",
"backgroundSpringtimeLakeNotes": "Take in the sights along the shores of a Springtime Lake.",
"backgrounds052022": "SET 96: Released May 2022",
"backgroundOnACastleWallText": "On A Castle Wall",
"backgroundOnACastleWallNotes": "Look out from On a Castle Wall.",
"backgroundCastleGateText": "Castle Gate",
"backgroundCastleGateNotes": "Stand guard at the Castle Gate.",
"backgroundEnchantedMusicRoomText": "Enchanted Music Room",
"backgroundEnchantedMusicRoomNotes": "Play in an Enchanted Music Room.",
"timeTravelBackgrounds": "Steampunk Backgrounds",
"backgroundAirshipText": "Airship",
"backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.",
+1
View File
@@ -31,6 +31,7 @@
"hallContributors": "Hall of Contributors",
"hallPatrons": "Hall of Patrons",
"noAdminAccess": "You don't have admin access.",
"noPrivAccess": "You don't have the required privileges.",
"userNotFound": "User not found.",
"invalidUUID": "UUID must be valid",
"title": "Title",
+6
View File
@@ -628,6 +628,8 @@
"weaponArmoirePinkLongbowNotes": "Be a cupid-in-training, mastering both archery and matters of the heart with this beautiful bow. Increases Perception by <%= per %> and Strength by <%= str %>. Enchanted Armoire: Independent Item.",
"weaponArmoireGardenersWateringCanText": "Watering Can",
"weaponArmoireGardenersWateringCanNotes": "You cant get far without water! Have an infinite supply on hand with this magic, refilling watering can. Increases Intelligence by <%= int %>. Enchanted Armoire: Gardener Set (Item 4 of 4).",
"weaponArmoireHuntingHornText": "Hunting Horn",
"weaponArmoireHuntingHornNotes": "Twooooo! Twoo! Twoo! Gather your party for an adventure or quest by playing this horn. Increases Strength by <%= str %> and Intelligence by <%= int %>. Enchanted Armoire: Musical Instrument Set 1 (Item 1 of 3)",
"armor": "armor",
"armorCapitalized": "Armor",
@@ -2426,6 +2428,10 @@
"shieldArmoireSoftVioletPillowNotes": "The clever warrior packs a pillow for any expedition. Protect yourself from procrastination-induced panic... even while you nap. Increases Intelligence by <%= int %>. Enchanted Armoire: Violet Loungewear Set (Item 3 of 3).",
"shieldArmoireGardenersSpadeText": "Gardener's Spade",
"shieldArmoireGardenersSpadeNotes": "Whether youre digging in the garden, searching for buried treasure, or creating a secret tunnel, this trusty spade is at your side. Increases Strength by <%= str %>. Enchanted Armoire: Gardener Set (Item 3 of 4).",
"shieldArmoireSpanishGuitarText": "Spanish Guitar",
"shieldArmoireSpanishGuitarNotes": "Tink! Tink! Thrummm! Gather your party for a concert or celebration by playing this guitar. Increases Perception by <%= per %> and Intelligence by <%= int %>. Enchanted Armoire: Musical Instrument Set 1 (Item 2 of 3)",
"shieldArmoireSnareDrumText": "Snare Drum",
"shieldArmoireSnareDrumNotes": "Rat-a-tat-tat! Gather your party for a parade or march into battle by playing this drum. Increases Constitution by <%= con %> and Intelligence by <%= int %>. Enchanted Armoire: Musical Instrument Set 1 (Item 3 of 3)",
"back": "Back Accessory",
"backBase0Text": "No Back Accessory",
+4 -3
View File
@@ -672,8 +672,8 @@
"backgroundWinterWaterfallNotes": "Maravíllate en la catarata invernal.",
"backgroundIridescentCloudsText": "Nubes iridiscentes",
"backgroundIridescentCloudsNotes": "Flota entre nubes iridiscentes.",
"backgroundOrangeGroveText": "Campo de naranjos",
"backgroundOrangeGroveNotes": "Pasea por un fragante campo de naranjos.",
"backgroundOrangeGroveText": "Naranjal",
"backgroundOrangeGroveNotes": "Deambula por un naranjal fragante.",
"backgrounds022022": "93.ª serie: publicada en febrero de 2022",
"backgrounds032022": "94.ª serie: publiccada en marzo de 2022",
"backgroundBrickWallWithIvyText": "Pared de Ladrillo con Hiedra",
@@ -688,5 +688,6 @@
"backgroundFloweringPrairieNotes": "Brinca por una pradera floreciente.",
"backgroundFloweringPrairieText": "Pradera floreciente",
"backgroundSpringtimeLakeText": "Lago de Primavera",
"backgroundSpringtimeLakeNotes": "Disfruta las vistas a orillas de un Lago de Primavera."
"backgroundSpringtimeLakeNotes": "Disfruta las vistas a orillas de un Lago de Primavera.",
"hideLockedBackgrounds": "Esconde fondos cerrados"
}
+2 -1
View File
@@ -186,5 +186,6 @@
"communityInstagram": "Instagram",
"minPasswordLength": "La contraseña debe contener 8 caracteres o más.",
"enterHabitica": "Adéntrate en Habitica",
"emailUsernamePlaceholder": "p.e., habitrabbit o gryphon@example.com"
"emailUsernamePlaceholder": "p.e., habitrabbit o gryphon@example.com",
"socialAlreadyExists": "Esta identificación social ya está vinculado a una cuenta Habitica existente."
}
+5 -4
View File
@@ -164,14 +164,14 @@
"summer2019ConchHealerSet": "Caracola (Sanador)",
"summer2019WaterLilyMageSet": "Nenúfar (Mago)",
"summer2019SeaTurtleWarriorSet": "Tortuga Marina (Guerrero)",
"augustYYYY": "Agosto del <%= year %>",
"augustYYYY": "Agosto <%= year %>",
"decemberYYYY": "Diciembre <%= year %>",
"winter2020LanternSet": "Linterna (Pícaro)",
"winter2020WinterSpiceSet": "Especia de Invierno (Sanador)",
"winter2020CarolOfTheMageSet": "Villancico del Mago",
"winter2020EvergreenSet": "Siempre Joven (Guerrero)",
"mayYYYY": "Mayo del <%= year %>",
"marchYYYY": "Marzo del <%= year %>",
"mayYYYY": "Mayo <%= year %>",
"marchYYYY": "Marzo <%= year %>",
"summer2020CrocodileRogueSet": "Cocodrilo (Pícaro)",
"summer2020SeaGlassHealerSet": "Cristal Marino (Sanador)",
"summer2020OarfishMageSet": "Regaleco (Mago)",
@@ -220,5 +220,6 @@
"spring2022MagpieRogueSet": "Urraca (Pícaro)",
"spring2022RainstormWarriorSet": "Tempestad (Guerrero)",
"spring2022ForsythiaMageSet": "Forsitia (Mago)",
"spring2022PeridotHealerSet": "Peridoto (Sanador)"
"spring2022PeridotHealerSet": "Peridoto (Sanador)",
"aprilYYYY": "Abril <%= year %>"
}
+27 -1
View File
@@ -662,5 +662,31 @@
"backgroundWinterCanyonText": "Cañón Invernal",
"backgroundIcePalaceText": "Palacio de Hielo",
"backgroundIcePalaceNotes": "Reina desde el Palacio de Hielo.",
"backgrounds012022": "CONJUNTO 92: Lanzado en Enero 2022"
"backgrounds012022": "CONJUNTO 92: Lanzado en Enero 2022",
"backgrounds022022": "CONJUNTO 93: Lanzado en Febrero 2022",
"backgroundMeteorShowerText": "Lluvia de meteoritos",
"backgroundMeteorShowerNotes": "Observa el espectáculo nocturno deslumbrante de una lluvia de meteoritos.",
"backgroundPalmTreeWithFairyLightsText": "Palmera con una guirnalda luminosa",
"backgroundSnowyFarmText": "Granja Nevada",
"backgroundSnowyFarmNotes": "Comprueba que todo el mundo es calentito y a gusto en tu Granja Nevada.",
"backgroundPalmTreeWithFairyLightsNotes": "Posa junto a una palmera decorada con una guirnalda luminosa.",
"backgroundWinterWaterfallText": "Catarata invernal",
"backgroundWinterWaterfallNotes": "Maravíllate en la catarata invernal.",
"backgroundOrangeGroveText": "Naranjal",
"backgroundOrangeGroveNotes": "Deambula por un naranjal fragante.",
"backgroundIridescentCloudsText": "Nubes iridiscentes",
"backgroundIridescentCloudsNotes": "Flota entre nubes iridiscentes.",
"backgrounds032022": "CONJUNTO 94: Lanzado en Marzo 2022",
"backgroundAnimalsDenText": "Cubil de una Criatura del Bosque",
"backgroundAnimalsDenNotes": "Ponte cómodo en el Cubil de una Criatura del Bosque.",
"backgroundBrickWallWithIvyText": "Pared de Ladrillo con Hiedra",
"backgroundBrickWallWithIvyNotes": "Admira una Pared de Ladrillo con Hiedra.",
"backgroundFloweringPrairieNotes": "Brinca por una pradera floreciente.",
"backgrounds042022": "CONJUNTO 95: Lanzado en Abril 2022",
"backgroundBlossomingTreesText": "Árboles Florecidos",
"backgroundBlossomingTreesNotes": "Entretente bajo Árboles Florecidos.",
"backgroundFlowerShopText": "Tienda de Flores",
"backgroundFlowerShopNotes": "Disfruta el aroma suave de una Tienda de Flores.",
"backgroundSpringtimeLakeText": "Lago de Primavera",
"backgroundSpringtimeLakeNotes": "Disfruta las vistas a orillas de un Lago de Primavera."
}
+11 -1
View File
@@ -2459,5 +2459,15 @@
"weaponArmoirePotionBaseNotes": "Las mascotas que eclosionas con estas pociones serán muchas cosas, ¡pero no básicas! Aumenta la Fuerza, Inteligencia, Constitución y Percepción en <%= attrs %>. Armario Encantado: Conjunto de Pociones (Artículo 1 de 10)",
"weaponMystery202201Text": "Cañón de Confeti de Medianoche",
"weaponMystery202201Notes": "Libera una nube de dorada y plateada brillantina cuando el reloj toque la medianoche. ¡Feliz año nuevo! Y ¿quién va a limpiar esto? No otorga ningún beneficio. Artículo de suscriptor de Enero de 2022.",
"weaponMystery202111Notes": "Da forma al flujo temporal con este bastón misterioso y poderoso. No otorga ningún beneficio. Artículo de suscriptor de Noviembre 2021."
"weaponMystery202111Notes": "Da forma al flujo temporal con este bastón misterioso y poderoso. No otorga ningún beneficio. Artículo de suscriptor de Noviembre 2021.",
"weaponArmoirePotionWhiteText": "Poción Blanca Decorativa",
"weaponArmoirePotionWhiteNotes": "¡Las mascotas eclosionadas usando esta poción podrían perderse en la nieve! Aumenta la Constitución en <%= con %> y la Percepción en <%= per %>. Armario Encantado: Conjunto de Pociones (Artículo 2 de 10)",
"weaponArmoirePotionDesertText": "Poción de Desierto Decorativa",
"weaponArmoirePotionDesertNotes": "¡Con esta poción no te hará falta estar en una isla desierta para encontrar a una mascota color desierto con la que disfrutarla! Aumenta la Fuerza en <%= str %> y la Constitución en <%= con %>. Armario Encantado: Conjunto de Pociones (Artículo 3 de 10)",
"weaponArmoirePotionRedText": "Poción Roja Decorativa",
"weaponArmoirePotionSkeletonText": "Poción de Esqueleto Decorativa",
"weaponArmoirePotionRedNotes": "¡No te pongas colorado hoy, porque esta poción de eclosión no te dejará en números rojos! Aumenta la Fuerza y la Constitución en <%= attrs %>. Armario Encantado: Conjunto de Pociones (Artículo 4 de 10)",
"weaponArmoirePotionShadeText": "Poción de Sombra Decorativa",
"weaponArmoirePotionShadeNotes": "Como dice el refrán, a la sombra del favor, crecen vicios. ¡Y, a la sombra de esta poción, una mascota (a)sombrosa! Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Armario Encantado: Conjunto de Pociones (Artículo 5 de 10)",
"weaponArmoirePotionSkeletonNotes": "¿Te sientes productivo hoy? ¡Pues a mover el esqueleto! ¡No te olvides de llevarte esta poción de eclosión de esqueleto contigo! Aumenta la Fuerza en <%= str %> y la Inteligencia en <%= int %>. Armario Encantado: Conjunto de Pociones (Artículo 6 de 10)"
}
+2 -2
View File
@@ -217,6 +217,6 @@
"spring2022RainstormWarriorSet": "Tempestad (Guerrero)",
"spring2022ForsythiaMageSet": "Forsitia (Mago)",
"spring2022PeridotHealerSet": "Peridoto (Sanador)",
"januaryYYYY": "Enero <%= year %>",
"aprilYYYY": "Abril <%= year %>"
"januaryYYYY": "Enero, <%= year %>",
"aprilYYYY": "Abril, <%= year %>"
}
+3 -1
View File
@@ -114,5 +114,7 @@
"achievementPartyOn": "Lumaki ang iyong partido sa 4 na miyembro!",
"achievementKickstarter2019Text": "Sinuportahan ang 2019 Pin Kickstarter Project",
"achievementKickstarter2019": "Pin Kickstarter Backer",
"achievementAridAuthorityModalText": "Napaamo mo ang lahat ng Desert Mounts!"
"achievementAridAuthorityModalText": "Napaamo mo ang lahat ng Desert Mounts!",
"achievementDomesticated": "E-I-E-I-O",
"achievementDomesticatedText": "Napisâ ang lahát ng karaniwang kulay ng mga alagang hayop: Ferret, Guinea Pig, Rooster, Flying Pig, Daga, Bunny, Kabayo, at Baka!"
}
+46 -45
View File
@@ -1,5 +1,5 @@
{
"potionText": "Health Potion",
"potionText": "Mahiwagang Langís na Pámpalusóg",
"potionNotes": "Mag-recover ng 15 Health (Instant Use)",
"armoireText": "Enchanted Armoire",
"armoireNotesFull": "Open the Armoire to randomly receive special Equipment, Experience, or food! Equipment pieces remaining:",
@@ -183,35 +183,35 @@
"questEggVelociraptorMountText": "Velociraptor",
"questEggVelociraptorAdjective": "a clever",
"eggNotes": "Find a hatching potion to pour on this egg, and it will hatch into <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
"hatchingPotionBase": "Base",
"hatchingPotionWhite": "White",
"hatchingPotionDesert": "Desert",
"hatchingPotionRed": "Red",
"hatchingPotionShade": "Shade",
"hatchingPotionSkeleton": "Skeleton",
"hatchingPotionZombie": "Zombie",
"hatchingPotionCottonCandyPink": "Cotton Candy Pink",
"hatchingPotionCottonCandyBlue": "Cotton Candy Blue",
"hatchingPotionGolden": "Golden",
"hatchingPotionSpooky": "Spooky",
"hatchingPotionPeppermint": "Peppermint",
"hatchingPotionFloral": "Floral",
"hatchingPotionAquatic": "Aquatic",
"hatchingPotionEmber": "Ember",
"hatchingPotionThunderstorm": "Thunderstorm",
"hatchingPotionGhost": "Ghost",
"hatchingPotionRoyalPurple": "Royal Purple",
"hatchingPotionHolly": "Holly",
"hatchingPotionCupid": "Cupid",
"hatchingPotionShimmer": "Shimmer",
"hatchingPotionFairy": "Fairy",
"hatchingPotionStarryNight": "Starry Night",
"hatchingPotionRainbow": "Rainbow",
"hatchingPotionGlass": "Glass",
"hatchingPotionGlow": "Glow-in-the-Dark",
"hatchingPotionFrost": "Frost",
"hatchingPotionIcySnow": "Icy Snow",
"hatchingPotionNotes": "Pour this on an egg, and it will hatch as a <%= potText(locale) %> pet.",
"hatchingPotionBase": "Batayán",
"hatchingPotionWhite": "Putí",
"hatchingPotionDesert": "Desyerto",
"hatchingPotionRed": "Pulá",
"hatchingPotionShade": "Lilim",
"hatchingPotionSkeleton": "Kalansáy",
"hatchingPotionZombie": "Buháy na Bangkáy",
"hatchingPotionCottonCandyPink": "Kalimbahíng Minatamís na Bulak",
"hatchingPotionCottonCandyBlue": "Bugháw na Minatamís na Bulak",
"hatchingPotionGolden": "Ginintuán",
"hatchingPotionSpooky": "Nakakakilabot",
"hatchingPotionPeppermint": "Yerba Buwena",
"hatchingPotionFloral": "Mabulaklák",
"hatchingPotionAquatic": "Pantubig",
"hatchingPotionEmber": "Baga",
"hatchingPotionThunderstorm": "Bagyó",
"hatchingPotionGhost": "Multó",
"hatchingPotionRoyalPurple": "Maharlikáng Ube",
"hatchingPotionHolly": "Asebo",
"hatchingPotionCupid": "pidó",
"hatchingPotionShimmer": "Makináng",
"hatchingPotionFairy": "Maladiwatà",
"hatchingPotionStarryNight": "Mabituin na Gabí",
"hatchingPotionRainbow": "Bahaghari",
"hatchingPotionGlass": "Kristál",
"hatchingPotionGlow": "Naglíliwanag-sa-Dilím",
"hatchingPotionFrost": "Tigás-Lamíg",
"hatchingPotionIcySnow": "Mayelong Niyebe",
"hatchingPotionNotes": "Ibuhos mo itó sa isáng itlog, at mapípisâ itó bilang isang <%= potText(locale) %> na alagà.",
"premiumPotionAddlNotes": "Not usable on quest pet eggs.",
"foodMeat": "Meat",
"foodMeatThe": "the Meat",
@@ -303,9 +303,9 @@
"foodCandyRed": "Cinnamon Candy",
"foodCandyRedThe": "the Cinnamon Candy",
"foodCandyRedA": "Cinnamon Candy",
"foodSaddleText": "Saddle",
"foodSaddleNotes": "Instantly raises one of your pets into a mount.",
"foodSaddleSellWarningNote": "Hey! This is a pretty useful item! Are you familiar with how to use a Saddle with your Pets?",
"foodSaddleText": "Siya",
"foodSaddleNotes": "Agád na pinapalakí ang isa sa iyong mga alaga.",
"foodSaddleSellWarningNote": "Uy! Medyo kapaki-pakinabang! Pamilyar ka ba kung papaano gumamit ng Siya sa iyong mga Alagà?",
"foodNotes": "Feed this to a pet and it may grow into a sturdy steed.",
"foodPieRedA": "isang hiwa ng Pulang Cherry Pie",
"foodPieRedThe": "ang Pulang Cherry Pie",
@@ -338,18 +338,18 @@
"foodPieSkeletonThe": "ang Bone Marrow Pot Pie",
"foodPieSkeleton": "Bone Marrow Pot Pie",
"premiumPotionUnlimitedNotes": "Hindi magagamit sa quest pet eggs.",
"hatchingPotionMossyStone": "Malumot na Bato",
"hatchingPotionMossyStone": "Malumot na Bató",
"hatchingPotionPolkaDot": "Polka Dot",
"hatchingPotionStainedGlass": "Stained Glass",
"hatchingPotionBlackPearl": "Itim na Perlas",
"hatchingPotionAutumnLeaf": "Dahon ng Taglagas",
"hatchingPotionVampire": "Bampira",
"hatchingPotionBlackPearl": "Itím na Perlas",
"hatchingPotionAutumnLeaf": "Dahon ng Taglagás",
"hatchingPotionVampire": "Danag",
"hatchingPotionTurquoise": "Turkesa",
"hatchingPotionWindup": "Wind-Up",
"hatchingPotionWindup": "Susián",
"hatchingPotionSandSculpture": "Iskulturang Buhangin",
"hatchingPotionFluorite": "Fluorite",
"hatchingPotionDessert": "Confection",
"hatchingPotionBirchBark": "Birch Bark",
"hatchingPotionFluorite": "Fluorita",
"hatchingPotionDessert": "Minatamís",
"hatchingPotionBirchBark": "Balakbák ng Birch",
"hatchingPotionRuby": "Rubi",
"hatchingPotionAurora": "Aurora",
"hatchingPotionAmber": "Amber",
@@ -358,14 +358,15 @@
"hatchingPotionWatery": "Matubig",
"hatchingPotionBronze": "Tanso",
"hatchingPotionSunshine": "Sikat ng Araw",
"hatchingPotionVeggie": "Hardin",
"hatchingPotionCelestial": "Celestial",
"hatchingPotionRoseQuartz": "Rosas na Kuwarts",
"hatchingPotionVeggie": "Hálamanán",
"hatchingPotionCelestial": "Panlangit",
"hatchingPotionRoseQuartz": "Rosas na Kwarts",
"questEggRobotAdjective": "isang futuristic",
"questEggRobotMountText": "Robot",
"questEggRobotText": "Robot",
"questEggDolphinAdjective": "isang chipper",
"questEggDolphinMountText": "Dolphin",
"questEggDolphinText": "Dolphin",
"hatchingPotionSunset": "Paglubog ng Araw"
"hatchingPotionSunset": "Paglubóg ng Araw",
"hatchingPotionOnyx": "Onix"
}
+16 -16
View File
@@ -1,17 +1,17 @@
{
"pets": "Mga Alaga",
"pets": "Mga Alagà",
"stable": "Kuwadra",
"magicMounts": "Mga Kinabalang Sakay-Alaga",
"questMounts": "Lakbayang Sakay-Alaga",
"mountsTamed": "Mga Naamong Sakay-alaga",
"noActiveMount": "Walang Sinasakyang Alaga",
"activeMount": "Sinasakyang Alaga",
"mounts": "Mga Sakay Alaga",
"questPets": "Mga Alaga mula sa Quest",
"questMounts": "Lakbayang Sakáy-Alaga",
"mountsTamed": "Mga Naamong Sakáy-Alagà",
"noActiveMount": "Waláng Sinásakyang Alagà",
"activeMount": "Sinásakyang Alagà",
"mounts": "Mga Sakáy-Alagà",
"questPets": "Mga Alagang Hangò sa Pakikipagsápalarán",
"magicPets": "Mga Alagang Kinabalan",
"petsFound": "Mga Alagang Natagpuan",
"noActivePet": "Walang Aktibong Alaga",
"activePet": "Aktibong Alaga",
"petsFound": "Mga Alagang Natagpuán",
"noActivePet": "Waláng Aktibong Alagà",
"activePet": "Aktibong Alagà",
"food": "Pagkaing Pang-Alaga at Saddles",
"quickInventory": "Mabilisang Imbentaryo",
"haveHatchablePet": "Mayroon kang <%= potion %> hatching potion at <%= egg %> itlog upang ma-hatch ang alagang ito! <b>Pindutin</b> upang ma-hatch!",
@@ -28,11 +28,11 @@
"hopefulHippogriffMount": "Umaasang Hippogriff",
"hopefulHippogriffPet": "Umaasang Hippogriff",
"magicalBee": "Mahiwagang Bubuyog",
"phoenix": "Phoenix",
"royalPurpleGryphon": "Royal Purple Gryphon",
"phoenix": "Fenix",
"royalPurpleGryphon": "Kulay Ubeng Maharliká na Griffin",
"orca": "Orca",
"mammoth": "Mabalahibong Mammoth",
"mantisShrimp": "Tatampal",
"mantisShrimp": "Tatampál",
"hydra": "Hydra",
"cerberusPup": "Tutang Cerberus",
"veteranFox": "Beteranong Soro",
@@ -41,12 +41,12 @@
"veteranTiger": "Beteranong Tigre",
"veteranWolf": "Beteranong Lobo",
"etherealLion": "Ethereal na Leon",
"wackyPets": "Mga Wacky na Alaga",
"wackyPets": "Mga Alagang Katawá-tawá",
"beastMasterText2": " at pinakawalan ang kanilang mga alaga ng <%= count %> beses",
"beastMasterText": "Nahanap ang lahat ng 90 na alaga (napakahirap, batiin ang user na ito!)",
"beastMasterName": "Beast Master",
"beastMasterName": "Amo ng mga Halimaw",
"beastAchievement": "Nakamit mo ang \"Beast Master\" na Achievement sa pagkolekta ng lahat ng mga alaga!",
"beastMasterProgress": "Beast Master Progress",
"beastMasterProgress": "Ulat sa Pagiging Amo ng mga Halimaw",
"premiumPotionNoDropExplanation": "Ang mga Mahiwagang Hatching Potion ay hindi magagamit sa mga itlog na nakuha mula sa mga Quest. Ang nag-iisang paraan upang makakuha ng mga Mahiwagang Hatching Potion ay sa pagbili nito sa ibaba, hindi mula sa random drops.",
"dropsExplanationEggs": "Magwaldas ng mga Hiyas upang mas mabilis na makakuha ng mga itlog, kung ayaw mong hintaying ma-drop ang mga standard na itlog, o ulitin ang mga Quest upang makakuha ng Quest eggs. <a href=\"http://habitica.fandom.com/wiki/Drops\">Matuto pa tungkol sa drop system.</a>",
"dropsExplanation": "Mas mabilis na makakuha ng ganitong mga gamit sa pamamagitan ng mga Hiyas kung ayaw mong hintayin ang pag-drop pagtapos ng kada gawain. <a href=\"http://habitica.fandom.com/wiki/Drops\">Matuto pa tungkol sa drop system.</a>",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"quests": "Quests",
"quests": "Pakikipagsápalarán",
"quest": "quest",
"petQuests": "Pang-alaga at Mount Quests",
"unlockableQuests": "Unlockable Quests",
+59 -59
View File
@@ -17,33 +17,33 @@
"yellowred": "Weak",
"greenblue": "Strong",
"edit": "Edit",
"save": "Save",
"addChecklist": "Add Checklist",
"checklist": "Checklist",
"newChecklistItem": "New checklist item",
"expandChecklist": "Expand Checklist",
"collapseChecklist": "Collapse Checklist",
"text": "Title",
"save": "Iimbák",
"addChecklist": "Magdagdág ng Talaán ng mga Hakbáng",
"checklist": "Talaán ng mga Hakbáng",
"newChecklistItem": "Bagong Talâ",
"expandChecklist": "Ibukás ang Talaán ng mga Hakbáng",
"collapseChecklist": "Itiklóp ang Talaán ng mga Hakbáng",
"text": "Pamagát",
"notes": "Notes",
"advancedSettings": "Advanced Settings",
"difficulty": "Difficulty",
"difficulty": "Gaano Kahirap",
"difficultyHelp": "Inilalarawan ng pagkahirap kung ano ang antas ng isang Gawi, Daily, o To Do upang makumpleto mo. Ang mas mataas na pagkahirap ay nagreresulta ng mas malaking gantimpala pagkatapos ng isang Gawain, pero mas malaki rin ang pagbawas sa buhay tuwing may nalalampasang Daily o may masamang Gawi na napipindot.",
"trivial": "Trivial",
"easy": "Easy",
"trivial": "Napakasisiw",
"easy": "Madalíng Isagawâ",
"medium": "Medium",
"hard": "Hard",
"hard": "Mahirap Isagawâ",
"attributes": "Stats",
"progress": "Progress",
"daily": "Daily",
"dailies": "Dailies",
"dailysDesc": "Dailies repeat on a regular basis. Choose the schedule that works best for you!",
"daily": "Pang-Araw-Araw",
"dailies": "Mga Pang-Araw-Araw",
"dailysDesc": "Madalás umuulit ang mga Pang-Araw-Araw. Piliin ang talatakdaán na pinakamainam para sa iyo!",
"streakCounter": "Streak Counter",
"repeat": "Repeat",
"repeats": "Repeats",
"repeatEvery": "Repeat Every",
"repeatOn": "Repeat On",
"repeat": "Ulitin",
"repeats": "Inuulit",
"repeatEvery": "Ulitin Bawat",
"repeatOn": "Ulitin Sa",
"day": "Day",
"days": "Days",
"days": "Mga Araw",
"restoreStreak": "Adjust Streak",
"resetStreak": "Reset Streak",
"todo": "To Do",
@@ -51,23 +51,23 @@
"todosDesc": "Kailangang kumpletuhin nang isang beses ang mga To Do. Magdagdag ng mga listahan sa iyong mga To Do upang tumaas ang kanilang halaga.",
"dueDate": "Due Date",
"remaining": "Active",
"complete": "Done",
"complete": "Natapos",
"complete2": "Complete",
"today": "Today",
"today": "Ngayóng Araw na Itó",
"dueIn": "Due <%= dueIn %>",
"due": "Due",
"notDue": "Not Due",
"grey": "Grey",
"grey": "Kulay Abó",
"score": "Score",
"reward": "Reward",
"rewards": "Rewards",
"reward": "Pabuyà",
"rewards": "Mga Pabuyà",
"rewardsDesc": "Rewards are a great way to use Habitica and complete your tasks. Try adding a few today!",
"gold": "Gold",
"silver": "Silver (100 silver = 1 gold)",
"price": "Price",
"price": "Halagá",
"tags": "Tags",
"editTags": "Edit",
"newTag": "New Tag",
"newTag": "Bagong Taták",
"editTags2": "Edit Tags",
"toRequired": "You must supply a \"to\" property",
"startDate": "Start Date",
@@ -76,58 +76,58 @@
"streakText": "Has performed <%= count %> 21-day streaks on Dailies",
"streakSingular": "Streaker",
"streakSingularText": "Has performed a 21-day streak on a Daily",
"perfectName": "<%= count %> Perfect Days",
"perfectName": "Mga Pagpayag",
"perfectText": "Completed all active Dailies on <%= count %> days. With this achievement you get a +level/2 buff to all Stats for the next day. Levels greater than 100 don't have any additional effects on buffs.",
"perfectSingular": "Perfect Day",
"perfectSingular": "Waláng Kápaltós-Paltós na Araw",
"perfectSingularText": "Completed all active Dailies in one day. With this achievement you get a +level/2 buff to all Stats for the next day. Levels greater than 100 don't have any additional effects on buffs.",
"fortifyName": "Fortify Potion",
"fortifyPop": "Return all tasks to neutral value (yellow color), and restore all lost Health.",
"fortify": "Fortify",
"fortifyPop": "Ibalík ang lahat ng gawain sa gitnáng halaga (kulay diláw), at ibalik ang lahat ng nawawalang Kalusugan.",
"fortify": "Pagtibayin",
"fortifyComplete": "Fortify complete!",
"deleteTask": "Delete this Task",
"sureDelete": "Are you sure you want to delete this task?",
"streakCoins": "Streak Bonus!",
"taskToTop": "To top",
"taskToBottom": "To bottom",
"taskToBottom": "Ipailalim",
"taskAliasAlreadyUsed": "Task alias already used on another task.",
"taskNotFound": "Task not found.",
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
"invalidTasksType": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\".",
"invalidTasksTypeExtra": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\", \"completedTodos\".",
"cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.",
"cantDeleteChallengeTasks": "Hindi maaaring ilipat ang iisáng gawaing pagmamay-arì ng isang hamon.",
"checklistOnlyDailyTodo": "Suportado lamang ang mga listahan sa Dailies at mga To Do",
"checklistItemNotFound": "No checklist item was found with given id.",
"itemIdRequired": "\"itemId\" must be a valid UUID.",
"tagNotFound": "No tag item was found with given id.",
"tagIdRequired": "\"tagId\" must be a valid UUID corresponding to a tag belonging to the user.",
"tagIdRequired": "Ang \"tagId\" ay dapat wastó na UUID na buhat sa taták na pagmamay-ari ng tagagamit.",
"positionRequired": "\"position\" is required and must be a number.",
"cantMoveCompletedTodo": "Can't move a completed todo.",
"cantMoveCompletedTodo": "Hindi maaaring ilipat ang mga natapos na na dapat gawín.",
"directionUpDown": "\"direction\" is required and must be 'up' or 'down'.",
"alreadyTagged": "The task is already tagged with given tag.",
"taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested",
"taskApprovalHasBeenRequested": "Approval has been requested",
"taskApprovalWasNotRequested": "Only a task waiting for approval can be marked as needing more work",
"approvals": "Approvals",
"approvalRequired": "Needs Approval",
"weekly": "Weekly",
"monthly": "Monthly",
"yearly": "Yearly",
"summary": "Summary",
"repeatsOn": "Repeats On",
"dayOfWeek": "Day of the Week",
"dayOfMonth": "Day of the Month",
"month": "Month",
"months": "Months",
"week": "Week",
"weeks": "Weeks",
"year": "Year",
"years": "Years",
"resets": "Resets",
"nextDue": "Next Due Dates",
"checkOffYesterDailies": "Check off any Dailies you did yesterday:",
"yesterDailiesCallToAction": "Start My New Day!",
"sessionOutdated": "Your session is outdated. Please refresh or sync.",
"errorTemporaryItem": "This item is temporary and cannot be pinned.",
"alreadyTagged": "Nabigyán na ng urì na ang gawain gamit ang ibinigay na taták.",
"taskRequiresApproval": "Dapat mapayagan muna ang gawaing itó bago mo itó matapos. Ipinakiusap na ang pagpapapayag",
"taskApprovalHasBeenRequested": "Ipinakiusap ang pagpapapayag",
"taskApprovalWasNotRequested": "Hindí ipinakiusap ang pagpapapayag ukol sa gawaing itó.",
"approvals": "Mga Pagpayag",
"approvalRequired": "Nangangailangan ng Pagpayag",
"weekly": "Linggó-Linggó",
"monthly": "Buwán-Buwán",
"yearly": "Taón-Taón",
"summary": "Buód",
"repeatsOn": "Inuulit Tuwíng",
"dayOfWeek": "Araw ng Linggó",
"dayOfMonth": "Araw ng Buwán",
"month": "Buwán",
"months": "Mga Buwán",
"week": "Linggó",
"weeks": "Mga Linggó",
"year": "Taón",
"years": "Mga Taón",
"resets": "Mga Pagsasaayos Mulí",
"nextDue": "Mga Susunod na Nakatakdáng Petsa",
"checkOffYesterDailies": "Markahán ang ano mang Pang-Araw-Araw na ginawá mo kahapon:",
"yesterDailiesCallToAction": "Simulán ang Bagong Araw Ko!",
"sessionOutdated": "Lumà na ang iyóng <i>session</i>. Mangyari lamang na mag-<i>refresh</i> mag-<i>sync</i>.",
"errorTemporaryItem": "Pánsamantalá lamang ang gamit na itó at hindi maaaring idikít.",
"pressEnterToAddTag": "Pindutin ang Enter upang magdagdag ng tag: '<%= tagName %>'",
"enterTag": "Maglagay ng tag",
"addTags": "Magdagdag ng tags...",
+9 -1
View File
@@ -685,5 +685,13 @@
"backgroundFloweringPrairieText": "Prairie fleurie",
"backgroundFloweringPrairieNotes": "Gambadez dans une prairie fleurie.",
"backgroundBrickWallWithIvyText": "Mur de brique avec Lierre",
"backgroundAnimalsDenText": "Tanière de créature des bois"
"backgroundAnimalsDenText": "Tanière de créature des bois",
"backgrounds042022": "Ensemble 95 : sorti en avril 2022",
"backgroundBlossomingTreesText": "Arbres en fleurs",
"hideLockedBackgrounds": "Cacher les arrière-plans verrouillés",
"backgroundBlossomingTreesNotes": "Promenez-vous au milieu des arbres en fleurs.",
"backgroundFlowerShopText": "Boutique de fleurs",
"backgroundSpringtimeLakeNotes": "Profitez de la vue sur les rives d'un lac printanier.",
"backgroundFlowerShopNotes": "Profitez de la douce senteur d'une boutique de fleurs.",
"backgroundSpringtimeLakeText": "Lac printanier"
}
+5 -1
View File
@@ -2614,5 +2614,9 @@
"eyewearMystery202204BText": "Visage virtuel",
"eyewearMystery202204BNotes": "Quelle est votre humeur du jour ? Exprimez vous avec ces écrans amusants. Ne confère aucun bonus. Équipement d'abonnement d'avril 2022.",
"eyewearMystery202204AText": "Visage virtuel",
"eyewearMystery202204ANotes": "Quelle est votre humeur du jour ? Exprimez vous avec ces écrans amusants. Ne confère aucun bonus. Équipement d'abonnement d'avril 2022."
"eyewearMystery202204ANotes": "Quelle est votre humeur du jour ? Exprimez vous avec ces écrans amusants. Ne confère aucun bonus. Équipement d'abonnement d'avril 2022.",
"armorArmoireStrawRaincoatText": "Ciret de paille",
"armorArmoireStrawRaincoatNotes": "Cette cape en paille tressée vous gardera au sec et évitera à votre armure de rouiller pendant les quêtes. Mais ne vous aventurez pas trop près des bougies ! Augmente la constitution de <%= con %>. Armoire enchantée : ensemble de pluie en paille (objet 1 de 2).",
"headArmoireStrawRainHatText": "Chapeau de pluie en paille",
"headArmoireStrawRainHatNotes": "Vous pourrez détecter tous les obstacles sur votre chemin en portant ce chapeau imperméable et canonique. Augmente la perception de <%= per %>. Armoire enchantée : ensemble de pluie en paille (objet 2 de 2)."
}
+2 -1
View File
@@ -220,5 +220,6 @@
"spring2022MagpieRogueSet": "Pie (Voleur)",
"spring2022RainstormWarriorSet": "Flot diluvien (Guerrier)",
"spring2022ForsythiaMageSet": "Forsythia (Mage)",
"spring2022PeridotHealerSet": "Péridot (Guérisseur)"
"spring2022PeridotHealerSet": "Péridot (Guérisseur)",
"aprilYYYY": "Avril <%= year %>"
}

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