Files
habitica/website/server/models/user/schema.js
Kalista Payne 384fb505c1 Privacy Controls (#15492)
* WIP(privacy): start of banner

* WIP(privacy): layout rough

* WIP(privacy): mobile layout, add modal

* fix(privacy): implement toggle disable and setting row fold

* fix(privacy): clean up a couple of styles

* fix(privacy): adjust banner width at mobile sizes

* WIP(privacy): remove Loggly echo of Amplitude data

* fix(banners): account for privacy in snackbar position

* WIP(privacy): dismiss banner

* chore(analytics): update to maintaned GA4 library

* fix(tests): lint, misuse of apiError

* fix(analytics): add debug mode

* fix(analytics): load new library on client

* WIP(privacy): gtag.js based implementation

* fix(analytics): lint issues

* fix(lint): one more unused

* fix(lint): client errors

* feat(privacy): draft workflows

* fix(analytics): linting, send needed user values

* fix(tests): use mock analytics service in test env

* fix(tests): restore previous logic for node env

* feat(intro): jump to page 2 onboarding

* WIP(auth): revisions to registration flow

* WIP(privacy): landing page and banner revisions

* WIP(signup): added new username, tos, privacy state

* fix(signup): revert debugging logic

* WIP(signup): add defaulting and checkbox

* wip(signup): move social auth behind username screen

* Squashed commit of the following:

commit ca0a238e5f008525ed154c5eaf12e44f2fc22b00
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed May 7 12:17:20 2025 +0200

    make emails lowercase

commit a2ce748558ce9134e6825208a7e66d78e720202e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Apr 9 13:27:01 2025 +0200

    remove unused import

commit cc6ce6c388d9693cf192c4bea733931fc8c31c37
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Apr 9 13:13:03 2025 +0200

    add tests for new api route

commit 0d40a6230b548625482aa9f6831c93ed9d62533a
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Wed Jun 18 15:50:22 2025 -0500

    update social tests

commit 79177d6754589b9e54682af8a531b63f60215dab
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Apr 9 10:21:51 2025 +0200

    new api route to check if an email is available

commit 11df73fe07eeb730c2a95593e18e14a931f52429
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Apr 9 10:21:39 2025 +0200

    Add field to not register social account when called

* Squashed commit of the following:

commit b8a2f0b8ee
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 20 17:18:30 2024 -0400

    update privacy policy

* fix(vite): import syntax

* feat(auth): precheck on defaulted username

* feat(auth): add store action for check-email

* feat(auth): check email before proceeding

* WIP(login): refactor username screen

* WIP(auth): complete login/reg flow

* fix(auth): filter out expected 404

* fix(login): use allowRegister with Apple
and add z-index to component

* fix(login): style corrections and email passthru

* Fix edgecase

Signed-off-by: Kalista Payne <sabrecat@gmail.com>

* fix(auth): correct error behaviors

* fix(auth): rewire Apple auth

* make check-email check for restricted domains

Signed-off-by: Kalista Payne <kalista@habitica.com>

* fix(signup): all the style

* fix(express): return when responding

* fix(error): reduce specificity for restricted domain issue

* fix apple auth

Signed-off-by: Kalista Payne <kalista@habitica.com>

* fix(signup): change from blur to 500ms debounce

* fix(login): add missing 200 response in Apple flow

* fix(signup): more reconciliation with @phillipthelen's work

* fix(signup): now using token not code

* fix(reg): don't bail on Apple if we're allowing reg

* fix(auth): more reconciliation with @phillipthelen code

* feat(copy): privacy policy updates

* fix(copy): replace placeholder

* fix(vue): use Vite syntax for scss import

* fix(static): corrections to copy and css

* chore(style): remove excess whitespace

* use correct error

Signed-off-by: Kalista Payne <kalista@habitica.com>

* fix(layout): inputs, add privacy banner

* fix(login): button hover, more validation states

* fix(login): further layout and UX corrections

* fix(static): add back containing div for show/hide

* fix(apple): clean out Apple token

* fix(settings): only change preference on save

* fix(settings): correct save/cancel behavior

* fix(layout): consistent use of header/footer

* fix(layout): reposition mountains for reg/login/forgot

* fix(signup): partial rollback of /username route

* refactor(signup): move /username to page

* fix(apple): don't overwrite reg method

* fix(username): don't skip empty validation

* fix(input): don't show valid if no username

* fix(login): clean out Apple token if using another method

* fix(apple): possible race with token

* fix(tests): some housekeeping

* fix(config): copypasta

* fix(lint): various cleanup

* fix(lint): line squeeze

* fix(lint): one more v-for

* fix(groups): funnel invite flow to new username page

* Squashed commit of the following:

commit 3c5ba4bf24e4bb7996786520101f27ad66405bce
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Aug 18 14:38:31 2025 -0500

    fix(privacy): update link ref

commit 9d216f623b
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Aug 18 14:18:22 2025 -0500

    fix(privacy-tos): copy edits cont'd

commit d744f47140
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Aug 18 13:43:22 2025 -0500

    fix(privacy): copy edits and ToC reflow

commit 2c3c3fc9ce
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Aug 18 18:46:24 2025 +0200

    lint

commit cf363034d5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Aug 18 18:34:54 2025 +0200

    fix link

commit 3afacd2c05
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Aug 18 18:34:42 2025 +0200

    add updated terms

commit 258b722499
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Aug 18 17:58:42 2025 +0200

    put back button to show/hide third party info

commit 2992e0299b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Aug 18 17:58:32 2025 +0200

    minor edits

commit bb5e252299
Author: Kalista Payne <kalista@habitica.com>
Date:   Sun Aug 17 21:01:50 2025 -0500

    fix(privacy): update Section 3

commit c79af7baa8
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Aug 15 17:28:49 2025 -0500

    fix(privacy): various copy edits

commit 100f2f4574
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Aug 15 11:37:37 2025 +0200

    add newline

commit 11d1cfd0d9
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Aug 15 11:10:01 2025 +0200

    update privacy policy

commit 59b99badf3
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Aug 8 14:04:19 2025 -0500

    5.38.2

commit 78daeb4191
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Aug 8 13:36:19 2025 -0500

    fix(apple): don't run auth middleware during redirect

commit 93f8d60903
Author: Weblate <noreply@weblate.org>
Date:   Fri Aug 8 10:12:25 2025 +0200

    Translated using Weblate (German)

    Currently translated at 99.4% (185 of 186 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (186 of 186 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (186 of 186 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (54 of 54 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (243 of 243 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (15 of 15 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (47 of 47 strings)

    Translated using Weblate (Dutch)

    Currently translated at 78.0% (2643 of 3385 strings)

    Translated using Weblate (Dutch)

    Currently translated at 40.8% (100 of 245 strings)

    Translated using Weblate (Polish)

    Currently translated at 89.9% (233 of 259 strings)

    Translated using Weblate (Dutch)

    Currently translated at 67.5% (175 of 259 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (110 of 110 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 20.8% (51 of 245 strings)

    Translated using Weblate (Turkish)

    Currently translated at 65.9% (60 of 91 strings)

    Translated using Weblate (Turkish)

    Currently translated at 65.9% (60 of 91 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 17.9% (44 of 245 strings)

    Co-authored-by: FingerTiao <787170918@qq.com>
    Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
    Co-authored-by: Karmelkowy <kicimeow.karmelio@gmail.com>
    Co-authored-by: Linsey Dunya Pastoor <sekai.creations@gmail.com>
    Co-authored-by: Mete Olmez <metezori27@gmail.com>
    Co-authored-by: Sefa Uğurlu <ugurlusefa2@gmail.com>
    Co-authored-by: Summer_GUI <heyang94@163.com>
    Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
    Co-authored-by: Weblate <noreply@weblate.org>
    Co-authored-by: innnko <ayakabooker@gmail.com>
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/tr/
    Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/death/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
    Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/nl/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/pl/
    Translation: Habitica/Backgrounds
    Translation: Habitica/Challenge
    Translation: Habitica/Communityguidelines
    Translation: Habitica/Contrib
    Translation: Habitica/Death
    Translation: Habitica/Defaulttasks
    Translation: Habitica/Faq
    Translation: Habitica/Front
    Translation: Habitica/Gear
    Translation: Habitica/Generic
    Translation: Habitica/Settings

commit eb16fec41e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Aug 6 22:08:07 2025 +0200

    Add interface to block ip-addresses or clients due to abuse (#15484)

    * Read IP blocks from database

    * begin building general blocking solution

    * add new frontend files

    * Add UI for managing blockers

    * correctly reset local data after creating blocker

    * Tweak wording

    * Add UI for managing blockers

    * restructure admin pages

    * improve test coverage

    * Improve blocker UI

    * add blocker to block emails from registration

    * lint fix

    * fix

    * lint fixes

    * fix import

    * add new permission for managing blockers

    * improve permission check

    * fix managing permissions from admin

    * improve navbar display for non fullAccess admin

    * update block error strings

    * lint fix

    * add option to errorHandler to skip logging

    * validate blocker value during input

    * improve blocker form display

    * chore(subproj): reconcile habitica-images

    * fix(scripts): use same Mongo version for dev/test

    * fix(whitespace): eof

    * documentation improvements

    * remove nconf import

    * remove old test

    ---------

    Co-authored-by: Kalista Payne <kalista@habitica.com>
    Co-authored-by: Kalista Payne <sabrecat@gmail.com>

commit 47d832bf12
Author: Fiz <34069775+Hafizzle@users.noreply.github.com>
Date:   Tue Aug 5 15:12:44 2025 -0500

    Add backend support for Hydra mount (#15482)

    * chore: update time travelers shop to display seasonal backgrounds

    * chore: update time travelers banner (note CSS borken rn)

    * chore: fix borken CSS and update logic in shop

    * chore: added isSubscribed function, not working

    * chore: isSubscribed working but no bg for subscribers

    * chore: logic and css updates

    * chore: update habitica-images

    * chore: add check for trinket

    * chore: more time traveler shop logicking

    * Add backend support for Hydra mount

    - Add Dragon-Hydra to special mounts in stable.js
      - Configure as contributor level 7 reward with canFind: true
      - Add GIF format support for mount sprites
      - Enable admin panel granting capability

    * Fix Vue template errors in timeTravelers component

    * Fix duplicate template block in timeTravelers component

    * add CSS for Hydra mount GIF sprites

    Added CSS rules for Mount_Head_Dragon-Hydra and Mount_Body_Dragon-Hydra GIF sprites

    * Remove the separate Hydra mount dimension declaration

    ---------

    Co-authored-by: CuriousMagpie <eilatan@gmail.com>

commit c03ab9855f
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Aug 5 14:31:05 2025 -0500

    5.38.1

commit 8f96b7b7fd
Author: Weblate <noreply@weblate.org>
Date:   Tue Aug 5 13:02:45 2025 +0200

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 17.1% (42 of 245 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 16.7% (41 of 245 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 16.3% (40 of 245 strings)

    Translated using Weblate (Polish)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Japanese)

    Currently translated at 98.8% (425 of 430 strings)

    Translated using Weblate (French)

    Currently translated at 99.4% (184 of 185 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 15.9% (39 of 245 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (268 of 268 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3385 of 3385 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (185 of 185 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.8% (3379 of 3385 strings)

    Translated using Weblate (Polish)

    Currently translated at 95.5% (128 of 134 strings)

    Translated using Weblate (Japanese)

    Currently translated at 94.7% (254 of 268 strings)

    Translated using Weblate (Polish)

    Currently translated at 94.0% (126 of 134 strings)

    Translated using Weblate (Japanese)

    Currently translated at 98.6% (424 of 430 strings)

    Translated using Weblate (Japanese)

    Currently translated at 98.3% (423 of 430 strings)

    Translated using Weblate (Japanese)

    Currently translated at 92.5% (798 of 862 strings)

    Translated using Weblate (Japanese)

    Currently translated at 92.4% (797 of 862 strings)

    Translated using Weblate (Japanese)

    Currently translated at 90.6% (781 of 862 strings)

    Translated using Weblate (Japanese)

    Currently translated at 91.9% (3112 of 3385 strings)

    Translated using Weblate (Japanese)

    Currently translated at 91.9% (3111 of 3385 strings)

    Translated using Weblate (Japanese)

    Currently translated at 94.0% (174 of 185 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (8 of 8 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 15.5% (38 of 245 strings)

    Translated using Weblate (Japanese)

    Currently translated at 91.6% (3104 of 3385 strings)

    Translated using Weblate (Japanese)

    Currently translated at 93.5% (173 of 185 strings)

    Translated using Weblate (Japanese)

    Currently translated at 99.6% (279 of 280 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Japanese)

    Currently translated at 89.2% (769 of 862 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Japanese)

    Currently translated at 94.4% (253 of 268 strings)

    Translated using Weblate (Japanese)

    Currently translated at 91.8% (170 of 185 strings)

    Translated using Weblate (Japanese)

    Currently translated at 97.9% (421 of 430 strings)

    Translated using Weblate (Japanese)

    Currently translated at 91.6% (3104 of 3385 strings)

    Translated using Weblate (Japanese)

    Currently translated at 93.6% (251 of 268 strings)

    Translated using Weblate (Japanese)

    Currently translated at 90.8% (168 of 185 strings)

    Translated using Weblate (Japanese)

    Currently translated at 82.4% (202 of 245 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (268 of 268 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (3385 of 3385 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 15.1% (37 of 245 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Japanese)

    Currently translated at 91.3% (3092 of 3385 strings)

    Translated using Weblate (Japanese)

    Currently translated at 92.5% (248 of 268 strings)

    Translated using Weblate (Japanese)

    Currently translated at 92.5% (248 of 268 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (193 of 193 strings)

    Translated using Weblate (Croatian)

    Currently translated at 100.0% (15 of 15 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Korean)

    Currently translated at 22.8% (56 of 245 strings)

    Translated using Weblate (Korean)

    Currently translated at 47.7% (128 of 268 strings)

    Translated using Weblate (Croatian)

    Currently translated at 45.1% (121 of 268 strings)

    Translated using Weblate (Korean)

    Currently translated at 71.9% (620 of 862 strings)

    Translated using Weblate (Croatian)

    Currently translated at 70.6% (609 of 862 strings)

    Translated using Weblate (Croatian)

    Currently translated at 75.0% (6 of 8 strings)

    Translated using Weblate (Korean)

    Currently translated at 67.6% (291 of 430 strings)

    Translated using Weblate (Korean)

    Currently translated at 52.8% (1788 of 3385 strings)

    Translated using Weblate (Croatian)

    Currently translated at 50.3% (1706 of 3385 strings)

    Translated using Weblate (Croatian)

    Currently translated at 51.7% (134 of 259 strings)

    Translated using Weblate (Czech)

    Currently translated at 92.8% (130 of 140 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 86.9% (233 of 268 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (94 of 94 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 100.0% (94 of 94 strings)

    Translated using Weblate (Danish)

    Currently translated at 92.1% (105 of 114 strings)

    Translated using Weblate (Czech)

    Currently translated at 89.4% (102 of 114 strings)

    Translated using Weblate (Czech)

    Currently translated at 83.5% (112 of 134 strings)

    Translated using Weblate (Spanish (Latin America))

    Currently translated at 71.6% (308 of 430 strings)

    Translated using Weblate (Spanish (Latin America))

    Currently translated at 100.0% (245 of 245 strings)

    Translated using Weblate (Serbian)

    Currently translated at 84.4% (49 of 58 strings)

    Translated using Weblate (Bulgarian)

    Currently translated at 51.4% (144 of 280 strings)

    Translated using Weblate (Swedish)

    Currently translated at 66.5% (286 of 430 strings)

    Translated using Weblate (Serbian)

    Currently translated at 65.5% (282 of 430 strings)

    Translated using Weblate (Slovak)

    Currently translated at 65.5% (282 of 430 strings)

    Translated using Weblate (Romanian)

    Currently translated at 66.7% (287 of 430 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 100.0% (430 of 430 strings)

    Translated using Weblate (Danish)

    Currently translated at 66.0% (284 of 430 strings)

    Translated using Weblate (Czech)

    Currently translated at 69.7% (300 of 430 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.7% (3377 of 3385 strings)

    Translated using Weblate (Swedish)

    Currently translated at 54.1% (1834 of 3385 strings)

    Translated using Weblate (Serbian)

    Currently translated at 50.6% (1714 of 3385 strings)

    Translated using Weblate (Slovak)

    Currently translated at 50.0% (1695 of 3385 strings)

    Translated using Weblate (Romanian)

    Currently translated at 60.5% (2050 of 3385 strings)

    Translated using Weblate (Hebrew)

    Currently translated at 38.4% (1301 of 3385 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (3385 of 3385 strings)

    Translated using Weblate (Danish)

    Currently translated at 54.0% (1829 of 3385 strings)

    Translated using Weblate (Czech)

    Currently translated at 59.6% (2020 of 3385 strings)

    Translated using Weblate (Swedish)

    Currently translated at 75.6% (140 of 185 strings)

    Translated using Weblate (Serbian)

    Currently translated at 73.5% (136 of 185 strings)

    Translated using Weblate (Slovak)

    Currently translated at 84.8% (157 of 185 strings)

    Translated using Weblate (Romanian)

    Currently translated at 78.9% (146 of 185 strings)

    Translated using Weblate (Portuguese)

    Currently translated at 82.1% (152 of 185 strings)

    Translated using Weblate (Italian)

    Currently translated at 91.8% (170 of 185 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (185 of 185 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 100.0% (185 of 185 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 100.0% (185 of 185 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (184 of 184 strings)

    Translated using Weblate (Danish)

    Currently translated at 77.2% (143 of 185 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 98.7% (242 of 245 strings)

    Translated using Weblate (Czech)

    Currently translated at 75.1% (139 of 185 strings)

    Translated using Weblate (Bulgarian)

    Currently translated at 74.5% (138 of 185 strings)

    Translated using Weblate (Czech)

    Currently translated at 8.1% (20 of 245 strings)

    Translated using Weblate (Swedish)

    Currently translated at 72.0% (621 of 862 strings)

    Translated using Weblate (Serbian)

    Currently translated at 65.1% (562 of 862 strings)

    Translated using Weblate (Slovak)

    Currently translated at 66.9% (577 of 862 strings)

    Translated using Weblate (Romanian)

    Currently translated at 77.7% (670 of 862 strings)

    Translated using Weblate (Portuguese)

    Currently translated at 70.0% (604 of 862 strings)

    Translated using Weblate (Polish)

    Currently translated at 67.1% (579 of 862 strings)

    Translated using Weblate (Italian)

    Currently translated at 86.8% (749 of 862 strings)

    Translated using Weblate (Indonesian)

    Currently translated at 86.0% (742 of 862 strings)

    Translated using Weblate (Hebrew)

    Currently translated at 66.1% (570 of 862 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 98.0% (845 of 862 strings)

    Translated using Weblate (Danish)

    Currently translated at 69.9% (603 of 862 strings)

    Translated using Weblate (Czech)

    Currently translated at 69.7% (601 of 862 strings)

    Translated using Weblate (Bulgarian)

    Currently translated at 66.3% (572 of 862 strings)

    Translated using Weblate (Serbian)

    Currently translated at 74.0% (305 of 412 strings)

    Translated using Weblate (Turkish)

    Currently translated at 100.0% (193 of 193 strings)

    Translated using Weblate (Danish)

    Currently translated at 90.0% (371 of 412 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (Swedish)

    Currently translated at 53.6% (139 of 259 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (Danish)

    Currently translated at 62.1% (161 of 259 strings)

    Translated using Weblate (Bulgarian)

    Currently translated at 54.0% (140 of 259 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 82.8% (222 of 268 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 99.4% (184 of 185 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 98.3% (241 of 245 strings)

    Translated using Weblate (Japanese)

    Currently translated at 91.3% (3092 of 3385 strings)

    Translated using Weblate (Japanese)

    Currently translated at 88.4% (237 of 268 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (134 of 134 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Japanese)

    Currently translated at 100.0% (243 of 243 strings)

    Translated using Weblate (Japanese)

    Currently translated at 82.4% (202 of 245 strings)

    Translated using Weblate (English (United Kingdom))

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (Japanese)

    Currently translated at 87.3% (234 of 268 strings)

    Translated using Weblate (Japanese)

    Currently translated at 86.4% (160 of 185 strings)

    Translated using Weblate (Japanese)

    Currently translated at 99.8% (913 of 914 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (268 of 268 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (3377 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (3385 of 3385 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (914 of 914 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (268 of 268 strings)

    Translated using Weblate (Russian)

    Currently translated at 88.5% (248 of 280 strings)

    Translated using Weblate (Spanish)

    Currently translated at 99.8% (3379 of 3385 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (862 of 862 strings)

    Co-authored-by: Ayaka Booker <ayakabooker@gmail.com>
    Co-authored-by: Chaotic Lawful <habitica@eusebius.fr>
    Co-authored-by: FingerTiao <787170918@qq.com>
    Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
    Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
    Co-authored-by: Karmelkowy <kicimeow.karmelio@gmail.com>
    Co-authored-by: Lio Zam <zerofux@web.de>
    Co-authored-by: Mika <isekai.chr@gmail.com>
    Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
    Co-authored-by: Summer_GUI <heyang94@163.com>
    Co-authored-by: Vera <verasmolinap@gmail.com>
    Co-authored-by: Weblate <noreply@weblate.org>
    Co-authored-by: Zhi Hao Li <zhihaoli000@gmail.com>
    Co-authored-by: Zuz Q <zuzannakunik@gmail.com>
    Co-authored-by: innnko <ayakabooker@gmail.com>
    Co-authored-by: 吳昀錡 <J1120241@gm.fdhs.tyc.edu.tw>
    Co-authored-by: 潘致翰 <happyq0908@gmail.com>
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
    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/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pl/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/character/tr/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/da/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/sr/
    Translate-URL: https://translate.habitica.com/projects/habitica/death/hr/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/cs/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/bg/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/cs/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/da/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/it/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/ro/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/sk/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/sr/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/sv/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/cs/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/da/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/he/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/hr/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/ro/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/sk/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/sr/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/sv/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/cs/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/da/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/es_419/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/ro/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/sk/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/sr/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/sv/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/bg/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
    Translate-URL: https://translate.habitica.com/projects/habitica/messages/sr/
    Translate-URL: https://translate.habitica.com/projects/habitica/npc/cs/
    Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/npc/pl/
    Translate-URL: https://translate.habitica.com/projects/habitica/overview/hr/
    Translate-URL: https://translate.habitica.com/projects/habitica/overview/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/pets/cs/
    Translate-URL: https://translate.habitica.com/projects/habitica/pets/da/
    Translate-URL: https://translate.habitica.com/projects/habitica/quests/en_GB/
    Translate-URL: https://translate.habitica.com/projects/habitica/quests/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/bg/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/cs/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/da/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/en_GB/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/he/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hr/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/id/
    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/ko/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pl/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ro/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sk/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sr/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sv/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/bg/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/da/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/hr/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/sv/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ko/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/tasks/cs/
    Translation: Habitica/Achievements
    Translation: Habitica/Backgrounds
    Translation: Habitica/Character
    Translation: Habitica/Content
    Translation: Habitica/Death
    Translation: Habitica/Faq
    Translation: Habitica/Front
    Translation: Habitica/Gear
    Translation: Habitica/Generic
    Translation: Habitica/Groups
    Translation: Habitica/Limited
    Translation: Habitica/Messages
    Translation: Habitica/Npc
    Translation: Habitica/Overview
    Translation: Habitica/Pets
    Translation: Habitica/Quests
    Translation: Habitica/Questscontent
    Translation: Habitica/Settings
    Translation: Habitica/Subscriber
    Translation: Habitica/Tasks

commit 1dde2674f6
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Mon Jun 16 16:43:56 2025 -0500

    fix(content): don't filter out the thing we want

commit 76122a8889
Author: Kalista Payne <sabrecat@gmail.com>
Date:   Wed Jun 4 14:28:27 2025 -0500

    fix(mobile): provide Challenge categories via API

commit 9e309a875e
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Jul 28 14:15:00 2025 -0500

    5.38.0

commit 09e3a394b8
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Jul 28 14:06:45 2025 -0500

    5.37.3

commit eba263360f
Author: Weblate <noreply@weblate.org>
Date:   Mon Jul 28 21:03:17 2025 +0200

    Translated using Weblate (German)

    Currently translated at 100.0% (134 of 134 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (134 of 134 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (3377 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (3377 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (3377 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (243 of 243 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (184 of 184 strings)

    Translated using Weblate (German)

    Currently translated at 98.6% (850 of 862 strings)

    Translated using Weblate (German)

    Currently translated at 99.8% (3373 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 99.8% (3373 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 99.8% (3373 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 99.5% (3361 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 99.5% (3361 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 99.5% (3361 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 99.4% (3360 of 3377 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (184 of 184 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (185 of 185 strings)

    Translated using Weblate (Polish)

    Currently translated at 67.1% (579 of 862 strings)

    Translated using Weblate (Polish)

    Currently translated at 67.1% (579 of 862 strings)

    Translated using Weblate (Polish)

    Currently translated at 100.0% (91 of 91 strings)

    Translated using Weblate (Polish)

    Currently translated at 100.0% (91 of 91 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (184 of 184 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (184 of 184 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (184 of 184 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (184 of 184 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (184 of 184 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (245 of 245 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (47 of 47 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (193 of 193 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 14.2% (35 of 245 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 13.8% (34 of 245 strings)

    Translated using Weblate (Chinese (Traditional))

    Currently translated at 13.0% (32 of 245 strings)

    Translated using Weblate (Hebrew)

    Currently translated at 2.0% (5 of 245 strings)

    Translated using Weblate (Hebrew)

    Currently translated at 66.1% (570 of 862 strings)

    Translated using Weblate (Portuguese)

    Currently translated at 54.1% (1830 of 3377 strings)

    Co-authored-by: FingerTiao <787170918@qq.com>
    Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
    Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
    Co-authored-by: Jonathan Niessen <37.friedrich@gmail.com>
    Co-authored-by: Karmelkowy <kicimeow.karmelio@gmail.com>
    Co-authored-by: Katharina <katharinaanna.wilding@gmail.com>
    Co-authored-by: Laura Fleckenstein <fleckenstein_laura@web.de>
    Co-authored-by: Omer I.S <omeritzicschwartz@gmail.com>
    Co-authored-by: Remigiusz Haziak <haziakremigiusz@gmail.com>
    Co-authored-by: Uwe B <hbtca@tunixgut.de>
    Co-authored-by: Weblate <noreply@weblate.org>
    Co-authored-by: Wellinton Cardoso <wmcardoso1@hotmail.com>
    Co-authored-by: cloudzzy <truskawka412@gmail.com>
    Co-authored-by: 吳昀錡 <J1120241@gm.fdhs.tyc.edu.tw>
    Translate-URL: https://translate.habitica.com/projects/habitica/character/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pl/
    Translate-URL: https://translate.habitica.com/projects/habitica/contrib/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/he/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
    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/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
    Translate-URL: https://translate.habitica.com/projects/habitica/generic/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/npc/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/he/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pl/
    Translation: Habitica/Character
    Translation: Habitica/Communityguidelines
    Translation: Habitica/Contrib
    Translation: Habitica/Faq
    Translation: Habitica/Front
    Translation: Habitica/Gear
    Translation: Habitica/Generic
    Translation: Habitica/Npc
    Translation: Habitica/Questscontent

commit 9550eec718
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jul 28 16:50:38 2025 +0200

    Fix 500 when deleting a very old group plan account (#15481)

commit f267eb67e9
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Jul 29 14:12:35 2025 -0500

    fix(static): add back missing div for show/hide

commit 28251f42ab
Author: Kalista Payne <kalista@habitica.com>
Date:   Thu Jul 24 22:59:01 2025 -0500

    feat(privacy): preview page

* feat(privacy): respect Global Privacy Control

* fix(lint): remove unused component

* fix(test): test user opts in to tracking

* fix(test): add user pref to more contexts

* fix(test): final spot in api-unit

* fix(tests): update integrations

* chore(privacy): add paragraph to s1, retire separate preview pages

* fix(build): route copypasta

* fix(router): lingering dead import

---------

Signed-off-by: Kalista Payne <sabrecat@gmail.com>
Signed-off-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: CuriousMagpie <eilatan@gmail.com>
2025-08-29 15:44:19 -05:00

767 lines
26 KiB
JavaScript

import { Schema } from 'mongoose';
import validator from 'validator';
import shared from '../../../common';
import { // eslint-disable-line import/no-cycle
getDefaultOwnedGear,
} from '../../libs/items/utils';
import {
RESTRICTED_EMAIL_DOMAINS,
isRestrictedEmailDomain,
} from '../../libs/auth/utils';
import { schema as PushDeviceSchema } from '../pushDevice';
import { schema as SubscriptionPlanSchema } from '../subscriptionPlan';
import { schema as TagSchema } from '../tag';
import { schema as UserNotificationSchema } from '../userNotification';
import { schema as WebhookSchema } from '../webhook';
import { model as Blocker } from '../blocker';
const BLOCKED_EMAILS = [];
Blocker.watchBlockers({
type: 'email',
area: 'full',
}, {
initial: true,
}).on('change', async change => {
const { operation, blocker: { value } } = change;
if (operation === 'add') {
if (value && !BLOCKED_EMAILS.includes(value)) {
BLOCKED_EMAILS.push(value);
}
} else if (operation === 'delete') {
const index = BLOCKED_EMAILS.indexOf(value);
if (index !== -1) {
BLOCKED_EMAILS.splice(index, 1);
}
}
});
// User schema definition
export const UserSchema = new Schema({
apiToken: {
$type: String,
default: shared.uuid,
},
auth: {
blocked: Boolean,
facebook: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
google: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
apple: { $type: Schema.Types.Mixed, default: () => ({}) },
local: {
email: {
$type: String,
validate: [{
validator: v => validator.isEmail(v),
message: shared.i18n.t('invalidEmail'),
}, {
validator (email) {
return !isRestrictedEmailDomain(email);
},
message: shared.i18n.t('invalidEmailDomain', { domains: RESTRICTED_EMAIL_DOMAINS.join(', ') }),
}, {
validator (email) {
const lowercaseEmail = email.toLowerCase();
return BLOCKED_EMAILS.every(block => lowercaseEmail.indexOf(block) === -1);
},
message: shared.i18n.t('emailBlockedRegistration'),
}],
},
username: {
$type: String,
},
// Store a lowercase version of username to check for duplicates
lowerCaseUsername: String,
hashed_password: String, // eslint-disable-line camelcase
// Legacy password are hashed with SHA1, new ones with bcrypt
passwordHashMethod: {
$type: String,
enum: ['bcrypt', 'sha1'],
},
salt: String, // Salt for SHA1 encrypted passwords, not stored for bcrypt,
// Used to validate password reset codes and make sure only the most recent one can be used
passwordResetCode: String,
},
timestamps: {
created: { $type: Date, default: Date.now },
loggedin: { $type: Date, default: Date.now },
updated: { $type: Date, default: Date.now },
},
},
// We want to know *every* time an object updates.
// Mongoose uses __v to designate when an object contains arrays which
// have been updated, but we want *every* update
_v: { $type: Number, default: 0 },
migration: String,
achievements: {
originalUser: Boolean,
habitSurveys: Number,
ultimateGearSets: {
healer: { $type: Boolean, default: false },
wizard: { $type: Boolean, default: false },
rogue: { $type: Boolean, default: false },
warrior: { $type: Boolean, default: false },
},
beastMaster: Boolean,
beastMasterCount: Number,
mountMaster: Boolean,
mountMasterCount: Number,
triadBingo: Boolean,
triadBingoCount: Number,
veteran: Boolean,
snowball: Number,
spookySparkles: Number,
shinySeed: Number,
seafoam: Number,
streak: { $type: Number, default: 0 },
challenges: Array,
quests: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
rebirths: Number,
rebirthLevel: Number,
perfect: { $type: Number, default: 0 },
habitBirthdays: Number,
valentine: Number,
nye: Number,
habiticaDays: Number,
greeting: Number,
thankyou: Number,
costumeContests: Number,
birthday: Number,
partyUp: Boolean,
partyOn: Boolean,
congrats: Number,
getwell: Number,
goodluck: Number,
royallyLoyal: Boolean,
joinedGuild: Boolean,
joinedChallenge: Boolean,
invitedFriend: Boolean,
lostMasterclasser: Boolean,
mindOverMatter: Boolean,
justAddWater: Boolean,
backToBasics: Boolean,
allYourBase: Boolean,
dustDevil: Boolean,
aridAuthority: Boolean,
kickstarter2019: Boolean,
monsterMagus: Boolean,
undeadUndertaker: Boolean,
primedForPainting: Boolean,
pearlyPro: Boolean,
tickledPink: Boolean,
rosyOutlook: Boolean,
bugBonanza: Boolean,
bareNecessities: Boolean,
freshwaterFriends: Boolean,
goodAsGold: Boolean,
allThatGlitters: Boolean,
boneCollector: Boolean,
skeletonCrew: Boolean,
seeingRed: Boolean,
redLetterDay: Boolean,
legendaryBestiary: Boolean,
seasonalSpecialist: Boolean,
violetsAreBlue: Boolean,
wildBlueYonder: Boolean,
domesticated: Boolean,
shadyCustomer: Boolean,
shadeOfItAll: Boolean,
zodiacZookeeper: Boolean,
birdsOfAFeather: Boolean,
reptacularRumble: Boolean,
woodlandWizard: Boolean,
boneToPick: Boolean,
polarPro: Boolean,
plantParent: Boolean,
dinosaurDynasty: Boolean,
bonelessBoss: Boolean,
duneBuddy: Boolean,
roughRider: Boolean,
rodentRuler: Boolean,
cats: Boolean,
// Onboarding Guide
createdTask: Boolean,
completedTask: Boolean,
hatchedPet: Boolean,
fedPet: Boolean,
purchasedEquipment: Boolean,
groupsBeta2022: Boolean,
},
backer: {
tier: Number,
npc: String,
tokensApplied: Boolean,
},
contributor: {
// 1-9, see https://habitica.fandom.com/wiki/Contributor_Rewards
level: {
$type: Number,
min: 0,
max: 9,
},
admin: Boolean,
// Artisan, Friend, Blacksmith, etc
text: String,
// a markdown textarea to list their contributions + links
contributions: String,
// user can own Critical Hammer of Bug-Crushing if this has a truthy value
critical: String,
},
permissions: {
fullAccess: Boolean, // esentially what was previously contributor.admin. Can do everything
news: Boolean,
userSupport: Boolean, // access User Support feature in Admin Panel
challengeAdmin: Boolean, // Can manage and administrate challenges
moderator: Boolean, // Can ban, flag users and manage social spaces
accessControl: Boolean, // Can manage IP and client blockers
coupons: Boolean, // Can generate and request coupons
},
balance: { $type: Number, default: 0 },
purchased: {
ads: { $type: Boolean, default: false },
// eg, {skeleton: true, pumpkin: true, eb052b: true}
skin: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
hair: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
shirt: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
background: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
txnCount: { $type: Number, default: 0 },
mobileChat: Boolean,
plan: {
$type: SubscriptionPlanSchema,
default: () => ({}),
},
},
flags: {
customizationsNotification: { $type: Boolean, default: false },
showTour: { $type: Boolean, default: true },
tour: {
// -1 indicates "uninitiated", -2 means "complete",
// any other number is the current tour step (0-index)
intro: { $type: Number, default: -1 },
classes: { $type: Number, default: -1 },
stats: { $type: Number, default: -1 },
tavern: { $type: Number, default: -1 },
party: { $type: Number, default: -1 },
guilds: { $type: Number, default: -1 },
challenges: { $type: Number, default: -1 },
market: { $type: Number, default: -1 },
pets: { $type: Number, default: -1 },
mounts: { $type: Number, default: -1 },
hall: { $type: Number, default: -1 },
equipment: { $type: Number, default: -1 },
groupPlans: { $type: Number, default: -1 },
},
tutorial: {
common: {
habits: { $type: Boolean, default: false },
dailies: { $type: Boolean, default: false },
todos: { $type: Boolean, default: false },
rewards: { $type: Boolean, default: false },
party: { $type: Boolean, default: false },
pets: { $type: Boolean, default: false },
gems: { $type: Boolean, default: false },
skills: { $type: Boolean, default: false },
classes: { $type: Boolean, default: false },
tavern: { $type: Boolean, default: false },
equipment: { $type: Boolean, default: false },
items: { $type: Boolean, default: false },
mounts: { $type: Boolean, default: false },
inbox: { $type: Boolean, default: false },
stats: { $type: Boolean, default: false },
},
ios: {
addTask: { $type: Boolean, default: false },
editTask: { $type: Boolean, default: false },
deleteTask: { $type: Boolean, default: false },
filterTask: { $type: Boolean, default: false },
groupPets: { $type: Boolean, default: false },
inviteParty: { $type: Boolean, default: false },
reorderTask: { $type: Boolean, default: false },
},
},
dropsEnabled: { $type: Boolean, default: false }, // unused
itemsEnabled: { $type: Boolean, default: false },
lastNewStuffRead: { $type: String, default: '' },
// The newStuff field was changed to be a computed property when returning the user in json,
// so that it doesn't have to be updated for each bailey post.
// See models/user/hooks#toJSONTransform
// newStuff: { $type: Boolean, default: false },
rewrite: { $type: Boolean, default: true },
classSelected: { $type: Boolean, default: false },
mathUpdates: Boolean,
rebirthEnabled: { $type: Boolean, default: false },
lastFreeRebirth: Date,
levelDrops: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
chatRevoked: Boolean,
chatShadowMuted: Boolean,
// Used to track the status of recapture emails sent to each user,
// can be 0 - no email sent - 1, 2, 3 or 4 - 4 means no more email will be sent to the user
recaptureEmailsPhase: { $type: Number, default: 0 },
// Needed to track the tip to send inside the email
weeklyRecapEmailsPhase: { $type: Number, default: 0 },
// Used to track when the next weekly recap should be sent
lastWeeklyRecap: { $type: Date, default: Date.now },
// Used to enable weekly recap emails as users login
lastWeeklyRecapDiscriminator: Boolean,
onboardingEmailsPhase: String, // Keep track of the latest onboarding email sent
communityGuidelinesAccepted: { $type: Boolean, default: false },
cronCount: { $type: Number, default: 0 },
welcomed: { $type: Boolean, default: false },
armoireEnabled: { $type: Boolean, default: true },
armoireOpened: { $type: Boolean, default: false },
armoireEmpty: { $type: Boolean, default: false },
cardReceived: { $type: Boolean, default: false },
warnedLowHealth: { $type: Boolean, default: false },
verifiedUsername: { $type: Boolean, default: false },
thirdPartyTools: { $type: Date },
initializedUserHistory: { $type: Boolean, default: false },
},
history: {
exp: Array, // [{date: Date, value: Number}], // big peformance issues if these are defined
todos: Array, // [{data: Date, value: Number}] // big peformance issues if these are defined
},
items: {
gear: {
owned: {
$type: Schema.Types.Mixed,
default: () => getDefaultOwnedGear(),
},
equipped: {
weapon: String,
armor: { $type: String, default: 'armor_base_0' },
head: { $type: String, default: 'head_base_0' },
shield: { $type: String, default: 'shield_base_0' },
back: String,
headAccessory: String,
eyewear: String,
body: String,
},
costume: {
weapon: String,
armor: { $type: String, default: 'armor_base_0' },
head: { $type: String, default: 'head_base_0' },
shield: { $type: String, default: 'shield_base_0' },
back: String,
headAccessory: String,
eyewear: String,
body: String,
},
},
special: {
snowball: { $type: Number, default: 0 },
spookySparkles: { $type: Number, default: 0 },
shinySeed: { $type: Number, default: 0 },
seafoam: { $type: Number, default: 0 },
valentine: { $type: Number, default: 0 },
valentineReceived: Array, // array of strings, by sender name
nye: { $type: Number, default: 0 },
nyeReceived: Array,
greeting: { $type: Number, default: 0 },
greetingReceived: Array,
thankyou: { $type: Number, default: 0 },
thankyouReceived: Array,
birthday: { $type: Number, default: 0 },
birthdayReceived: Array,
congrats: { $type: Number, default: 0 },
congratsReceived: Array,
getwell: { $type: Number, default: 0 },
getwellReceived: Array,
goodluck: { $type: Number, default: 0 },
goodluckReceived: Array,
},
// -------------- Animals -------------------
// Complex bit here. The result looks like:
// pets: {
// 'Wolf-Desert': 0, // 0 means does not own
// 'PandaCub-Red': 10, // Number represents "Growth Points"
// etc...
// }
pets: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
currentPet: { $type: String, default: '' }, // Cactus-Desert
// eggs: {
// 'PandaCub': 0, // 0 indicates "doesn't own"
// 'Wolf': 5 // Number indicates "stacking"
// }
eggs: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
// hatchingPotions: {
// 'Desert': 0, // 0 indicates "doesn't own"
// 'CottonCandyBlue': 5 // Number indicates "stacking"
// }
hatchingPotions: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
// Food: {
// 'Watermelon': 0, // 0 indicates "doesn't own"
// 'RottenMeat': 5 // Number indicates "stacking"
// }
food: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
// mounts: {
// 'Wolf-Desert': true,
// 'PandaCub-Red': false,
// etc...
// }
mounts: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
currentMount: { $type: String, default: '' }, // Cactus-Desert
// Quests: {
// 'boss_0': 0, // 0 indicates "doesn't own"
// 'collection_honey': 5 // Number indicates "stacking"
// }
quests: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
lastDrop: {
date: { $type: Date, default: Date.now },
count: { $type: Number, default: 0 },
},
},
lastCron: { $type: Date, default: Date.now },
_cronSignature: { $type: String, default: 'NOT_RUNNING' }, // Private property used to avoid double cron
// Lock property to avoid double subscription. Not strictly private because we query on it
_subSignature: { $type: String, default: 'NOT_RUNNING' },
// {GROUP_ID: Boolean}, represents whether they have unseen chat messages
newMessages: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
challenges: [{ $type: String, ref: 'Challenge', validate: [v => validator.isUUID(v), 'Invalid uuid for user challenges.'] }],
invitations: {
// Using an array without validation because otherwise mongoose
// treat this as a subdocument and applies _id by default
// Schema is (id, name, inviter, publicGuild)
// TODO one way to fix is https://mongoosejs.com/docs/guide.html#_id
guilds: { $type: Array, default: () => [] },
// Using a Mixed type because otherwise user.invitations.party = {}
// to reset invitation, causes validation to fail TODO
// schema is the same as for guild invitations (id, name, inviter)
party: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
parties: [{
id: {
$type: String,
ref: 'Group',
required: true,
validate: [v => validator.isUUID(v), 'Invalid uuid for user invitation party id.'],
},
name: {
$type: String,
required: true,
},
inviter: {
$type: String,
ref: 'User',
required: true,
validate: [v => validator.isUUID(v), 'Invalid uuid for user invitation inviter id.'],
},
cancelledPlan: {
$type: Boolean,
},
}],
},
guilds: [{ $type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid for user guild.'] }],
party: {
_id: { $type: String, validate: [v => validator.isUUID(v), 'Invalid uuid for user party.'], ref: 'Group' },
order: { $type: String, default: 'level' },
orderAscending: { $type: String, default: 'ascending' },
quest: {
key: String,
progress: {
up: { $type: Number, default: 0 },
down: { $type: Number, default: 0 },
collect: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
collectedItems: { $type: Number, default: 0 },
},
// When quest is done, we move it from key => completed,
// and it's a one-time flag (for modal) that they unset by clicking "ok" in browser
completed: String,
// Set to true when invite is pending, set to false when quest
// invite is accepted or rejected, quest starts, or quest is cancelled
RSVPNeeded: { $type: Boolean, default: false },
},
seeking: Date,
},
preferences: {
dayStart: {
$type: Number, default: 0, min: 0, max: 23,
},
size: { $type: String, enum: ['broad', 'slim'], default: 'slim' },
hair: {
color: { $type: String, default: 'red' },
base: { $type: Number, default: 3 },
bangs: { $type: Number, default: 1 },
beard: { $type: Number, default: 0 },
mustache: { $type: Number, default: 0 },
flower: { $type: Number, default: 1 },
},
hideHeader: { $type: Boolean, default: false },
skin: { $type: String, default: '915533' },
shirt: { $type: String, default: 'blue' },
timezoneOffset: { $type: Number, default: 0 },
sound: { $type: String, default: 'rosstavoTheme', enum: ['off', ...shared.content.audioThemes] },
chair: { $type: String, default: 'none' },
timezoneOffsetAtLastCron: Number,
language: String,
automaticAllocation: Boolean,
allocationMode: { $type: String, enum: ['flat', 'classbased', 'taskbased'], default: 'flat' },
autoEquip: { $type: Boolean, default: true },
costume: { $type: Boolean, default: false },
dateFormat: { $type: String, enum: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], default: 'MM/dd/yyyy' },
sleep: { $type: Boolean, default: false },
stickyHeader: { $type: Boolean, default: true },
disableClasses: { $type: Boolean, default: false },
newTaskEdit: { $type: Boolean, default: false },
// not used anymore, now the current filter is saved in preferences.activeFilter
dailyDueDefaultView: { $type: Boolean, default: false },
// deprecated, unused
advancedCollapsed: { $type: Boolean, default: false },
toolbarCollapsed: { $type: Boolean, default: false },
reverseChatOrder: { $type: Boolean, default: false },
developerMode: { $type: Boolean, default: false },
background: String,
// deprecated, unused
displayInviteToPartyWhenPartyIs1: { $type: Boolean, default: true },
webhooks: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
// For the following fields make sure to use strict
// comparison when searching for falsey values (=== false)
// As users who didn't login after these were introduced may have them undefined/null
emailNotifications: {
unsubscribeFromAll: { $type: Boolean, default: false },
newPM: { $type: Boolean, default: true },
kickedGroup: { $type: Boolean, default: true },
wonChallenge: { $type: Boolean, default: true },
giftedGems: { $type: Boolean, default: true },
giftedSubscription: { $type: Boolean, default: true },
invitedParty: { $type: Boolean, default: true },
invitedGuild: { $type: Boolean, default: true },
questStarted: { $type: Boolean, default: true },
invitedQuest: { $type: Boolean, default: true },
// remindersToLogin: {$type: Boolean, default: true},
// importantAnnouncements are in fact the recapture emails
importantAnnouncements: { $type: Boolean, default: true },
weeklyRecaps: { $type: Boolean, default: true },
onboarding: { $type: Boolean, default: true },
majorUpdates: { $type: Boolean, default: true },
subscriptionReminders: { $type: Boolean, default: true },
contentRelease: { $type: Boolean, default: true },
},
pushNotifications: {
unsubscribeFromAll: { $type: Boolean, default: false },
newPM: { $type: Boolean, default: true },
wonChallenge: { $type: Boolean, default: true },
giftedGems: { $type: Boolean, default: true },
giftedSubscription: { $type: Boolean, default: true },
invitedParty: { $type: Boolean, default: true },
invitedGuild: { $type: Boolean, default: true },
questStarted: { $type: Boolean, default: true },
invitedQuest: { $type: Boolean, default: true },
majorUpdates: { $type: Boolean, default: true },
mentionParty: { $type: Boolean, default: true },
mentionJoinedGuild: { $type: Boolean, default: true },
mentionUnjoinedGuild: { $type: Boolean, default: true },
partyActivity: { $type: Boolean, default: true },
contentRelease: { $type: Boolean, default: true },
},
suppressModals: {
levelUp: { $type: Boolean, default: false },
hatchPet: { $type: Boolean, default: false },
raisePet: { $type: Boolean, default: false },
streak: { $type: Boolean, default: false },
},
tasks: {
groupByChallenge: { $type: Boolean, default: false }, // @TODO remove? not used
confirmScoreNotes: { $type: Boolean, default: false }, // @TODO remove? not used
mirrorGroupTasks: [
{ $type: String, validate: [v => validator.isUUID(v), 'Invalid group UUID.'], ref: 'Group' },
],
activeFilter: {
habit: { $type: String, default: 'all' },
daily: { $type: String, default: 'all' },
todo: { $type: String, default: 'remaining' },
reward: { $type: String, default: 'all' },
},
},
improvementCategories: {
$type: Array,
validate: categories => {
const validCategories = ['work', 'exercise', 'healthWellness', 'school', 'teams', 'chores', 'creativity'];
const isValidCategory = categories
.every(category => validCategories.indexOf(category) !== -1);
return isValidCategory;
},
},
analyticsConsent: Boolean,
},
profile: {
blurb: String,
imageUrl: String,
name: {
$type: String,
required: true,
trim: true,
},
flags: { $type: Schema.Types.Mixed },
},
stats: {
hp: { $type: Number, default: shared.maxHealth },
mp: { $type: Number, default: 10, min: 0 },
exp: { $type: Number, default: 0 },
gp: { $type: Number, default: 0, min: 0 },
lvl: {
$type: Number,
default: 1,
min: 1,
max: shared.constants.MAX_LEVEL_HARD_CAP,
},
// Class System
class: {
$type: String, enum: shared.content.classes, default: 'warrior', required: true,
},
points: { $type: Number, default: 0, min: 0 },
str: { $type: Number, default: 0, min: 0 },
con: { $type: Number, default: 0, min: 0 },
int: { $type: Number, default: 0, min: 0 },
per: { $type: Number, default: 0, min: 0 },
buffs: {
str: { $type: Number, default: 0, min: 0 },
int: { $type: Number, default: 0, min: 0 },
per: { $type: Number, default: 0, min: 0 },
con: { $type: Number, default: 0, min: 0 },
stealth: { $type: Number, default: 0, min: 0 },
streaks: { $type: Boolean, default: false },
snowball: { $type: Boolean, default: false },
spookySparkles: { $type: Boolean, default: false },
shinySeed: { $type: Boolean, default: false },
seafoam: { $type: Boolean, default: false },
},
training: {
int: { $type: Number, default: 0 },
per: { $type: Number, default: 0 },
str: { $type: Number, default: 0 },
con: { $type: Number, default: 0 },
},
},
notifications: [UserNotificationSchema],
tags: [TagSchema],
inbox: {
// messages are stored in the Inbox collection
newMessages: { $type: Number, default: 0 },
blocks: { $type: Array, default: () => [] },
optOut: { $type: Boolean, default: false },
},
tasksOrder: {
habits: [{ $type: String, ref: 'Task' }],
dailys: [{ $type: String, ref: 'Task' }],
todos: [{ $type: String, ref: 'Task' }],
rewards: [{ $type: String, ref: 'Task' }],
},
extra: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
pushDevices: [PushDeviceSchema],
_ABtests: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
webhooks: [WebhookSchema],
loginIncentives: { $type: Number, default: 0 },
invitesSent: { $type: Number, default: 0 },
// Items manually pinned by the user
pinnedItems: [{
_id: false,
path: { $type: String },
type: { $type: String },
}],
// Ordered array of shown pinned items,
// necessary for sorting because seasonal items are not stored in pinnedItems
pinnedItemsOrder: [{ $type: String }],
// Items the user manually unpinned from the ones suggested by Habitica
unpinnedItems: [{
_id: false,
path: { $type: String },
type: { $type: String },
}],
// only visible to staff and moderators
secret: {
text: String,
},
}, {
skipVersioning: { notifications: true },
strict: true,
minimize: false, // So empty objects are returned
typeKey: '$type', // So that we can use fields named `type`
});
export default UserSchema; // fallback export until all imports using the Named one