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 b8a2f0b8ee12d065e3eb3ebfb2e720353e45607c 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 9d216f623b5dd65650726240f0197be96ec42075 Author: Kalista Payne <kalista@habitica.com> Date: Mon Aug 18 14:18:22 2025 -0500 fix(privacy-tos): copy edits cont'd commit d744f471404e6f83142c35fe603938dc07a66f9e Author: Kalista Payne <kalista@habitica.com> Date: Mon Aug 18 13:43:22 2025 -0500 fix(privacy): copy edits and ToC reflow commit 2c3c3fc9ce982c07c6db3d2a13f23b5005b75383 Author: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 18:46:24 2025 +0200 lint commit cf363034d554428102299bc8ae7aa6a596b25522 Author: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 18:34:54 2025 +0200 fix link commit 3afacd2c0569e01d251f71a850d8d2d223eb7d8a Author: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 18:34:42 2025 +0200 add updated terms commit 258b7224993f04362e6dce9afbb0959c35c95cd1 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 2992e0299b2d82fce4e5b153a3a9a3da1ee1c6b8 Author: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 17:58:32 2025 +0200 minor edits commit bb5e252299dcb6577a21ab092ac401420b1dc9aa Author: Kalista Payne <kalista@habitica.com> Date: Sun Aug 17 21:01:50 2025 -0500 fix(privacy): update Section 3 commit c79af7baa84bbdffe90e8aaa53da44501f5c2569 Author: Kalista Payne <kalista@habitica.com> Date: Fri Aug 15 17:28:49 2025 -0500 fix(privacy): various copy edits commit 100f2f4574fc47d1bcdbad4688616d0953a4051d Author: Phillip Thelen <phillip@habitica.com> Date: Fri Aug 15 11:37:37 2025 +0200 add newline commit 11d1cfd0d94f164d7a1f8f67dea5e83f4d7f39cc Author: Phillip Thelen <phillip@habitica.com> Date: Fri Aug 15 11:10:01 2025 +0200 update privacy policy commit 59b99badf3699b0d4f7a02ce4251ff1475878052 Author: Kalista Payne <kalista@habitica.com> Date: Fri Aug 8 14:04:19 2025 -0500 5.38.2 commit 78daeb4191b55486a80282ce82bb6456bba95e2f 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 93f8d60903383d1c888f901ae550d0410f89a855 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 eb16fec41eed42ac681ac0667ec7dc510670b5bc 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 47d832bf12dfdf0e1eb98f431edf5487e3ce6c3f 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 c03ab9855f055c1a5b44e1ca915af190764a2bad Author: Kalista Payne <kalista@habitica.com> Date: Tue Aug 5 14:31:05 2025 -0500 5.38.1 commit 8f96b7b7fdfc8a51db5b4cf8aa96d291e27388d4 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 1dde2674f6fb4664920c003eb827c8431e380479 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 76122a8889a6780cd66ee76194e64dc47ce1223f Author: Kalista Payne <sabrecat@gmail.com> Date: Wed Jun 4 14:28:27 2025 -0500 fix(mobile): provide Challenge categories via API commit 9e309a875e3025f3affb434a50c4dd3acaad64ac Author: Kalista Payne <kalista@habitica.com> Date: Mon Jul 28 14:15:00 2025 -0500 5.38.0 commit 09e3a394b810ad7820e4b100deb588586b304c23 Author: Kalista Payne <kalista@habitica.com> Date: Mon Jul 28 14:06:45 2025 -0500 5.37.3 commit eba263360f50d529fb28e3b9cad1f8058d83764f 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 9550eec718d284d6ff74aec6264a1ca7042135be 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 f267eb67e9af74129b86edf5092657377065b531 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 28251f42ab5f886ab7bae70c5a29563691c5c22c 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>
This commit is contained in:
committed by
Kalista Payne
parent
59ba07d4f3
commit
dd97b11b60
@@ -1,5 +1,6 @@
|
||||
import validator from 'validator';
|
||||
import moment from 'moment';
|
||||
import pick from 'lodash/pick';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
@@ -127,9 +128,9 @@ api.loginLocal = {
|
||||
await user.save();
|
||||
|
||||
res.analytics.track('login', {
|
||||
category: 'behaviour',
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
category: 'behavior',
|
||||
type: 'local',
|
||||
gaLabel: 'local',
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
});
|
||||
@@ -158,6 +159,9 @@ api.redirectApple = {
|
||||
async handler (req, res) {
|
||||
if (req.body.id_token) {
|
||||
req.body.network = 'apple';
|
||||
if (!req.body.allowRegister) {
|
||||
req.body.allowRegister = false;
|
||||
}
|
||||
return loginSocial(req, res);
|
||||
}
|
||||
let url = `/static/apple-redirect?code=${req.body.code}`;
|
||||
@@ -181,6 +185,8 @@ api.loginApple = {
|
||||
url: '/user/auth/apple',
|
||||
async handler (req, res) {
|
||||
req.body.network = 'apple';
|
||||
req.body.allowRegister = req.query.allowRegister === 'true';
|
||||
req.body.username = req.query.username;
|
||||
return loginSocial(req, res);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import merge from 'lodash/merge';
|
||||
import pick from 'lodash/pick';
|
||||
import reduce from 'lodash/reduce';
|
||||
import times from 'lodash/times';
|
||||
import { authWithHeaders, authWithSession } from '../../middlewares/auth';
|
||||
import { model as Challenge } from '../../models/challenge';
|
||||
import bannedWords from '../../libs/bannedWords';
|
||||
@@ -287,6 +291,7 @@ api.createChallenge = {
|
||||
response.group = getChallengeGroupResponse(group);
|
||||
|
||||
res.analytics.track('challenge create', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -355,6 +360,7 @@ api.joinChallenge = {
|
||||
response.leader = chalLeader ? chalLeader.toJSON({ minimize: true }) : null;
|
||||
|
||||
res.analytics.track('challenge join', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -405,6 +411,7 @@ api.leaveChallenge = {
|
||||
await challenge.unlinkTasks(user, keep);
|
||||
|
||||
res.analytics.track('challenge leave', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -519,7 +526,7 @@ api.getUserChallenges = {
|
||||
|
||||
if (search) {
|
||||
const searchOr = { $or: [] };
|
||||
const searchWords = _.escapeRegExp(search).split(' ').join('|');
|
||||
const searchWords = escapeRegExp(search).split(' ').join('|');
|
||||
const searchQuery = { $regex: new RegExp(`${searchWords}`, 'i') };
|
||||
searchOr.$or.push({ name: searchQuery });
|
||||
searchOr.$or.push({ description: searchQuery });
|
||||
@@ -775,14 +782,14 @@ api.exportChallengeCsv = {
|
||||
|
||||
// The first row is going to be UUID name Task Value Notes
|
||||
// repeated n times for the n challenge tasks
|
||||
const challengeTasks = _.reduce(
|
||||
const challengeTasks = reduce(
|
||||
challenge.tasksOrder.toObject(),
|
||||
(result, array) => result.concat(array),
|
||||
[],
|
||||
).sort();
|
||||
resArray.unshift(['UUID', 'Display Name', 'Username']);
|
||||
|
||||
_.times(challengeTasks.length, () => resArray[0].push('Task', 'Value', 'Notes', 'Streak'));
|
||||
times(challengeTasks.length, () => resArray[0].push('Task', 'Value', 'Notes', 'Streak'));
|
||||
|
||||
// Remove lines for users without tasks info
|
||||
resArray = resArray.filter(line => {
|
||||
@@ -847,7 +854,7 @@ api.updateChallenge = {
|
||||
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderUpdateChal'));
|
||||
group.purchased = undefined;
|
||||
_.merge(challenge, Challenge.sanitizeUpdate(req.body));
|
||||
merge(challenge, Challenge.sanitizeUpdate(req.body));
|
||||
|
||||
const savedChal = await challenge.save();
|
||||
const response = savedChal.toJSON();
|
||||
@@ -889,6 +896,7 @@ api.deleteChallenge = {
|
||||
await challenge.closeChal({ broken: 'CHALLENGE_DELETED' });
|
||||
|
||||
res.analytics.track('challenge delete', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -949,6 +957,7 @@ api.selectChallengeWinner = {
|
||||
await challenge.closeChal({ broken: 'CHALLENGE_CLOSED', winner });
|
||||
|
||||
res.analytics.track('challenge close', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pick from 'lodash/pick';
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
@@ -186,6 +187,7 @@ api.postChat = {
|
||||
// Check if account is newer than the minimum age for chat participation
|
||||
if (moment().diff(user.auth.timestamps.created, 'minutes') < ACCOUNT_MIN_CHAT_AGE) {
|
||||
analytics.track('chat age error', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -237,6 +239,7 @@ api.postChat = {
|
||||
await Promise.all(toSave);
|
||||
|
||||
const analyticsObject = {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import assign from 'lodash/assign';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import find from 'lodash/find';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import includes from 'lodash/includes';
|
||||
import isArray from 'lodash/isArray';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
import pick from 'lodash/pick';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
@@ -161,6 +169,7 @@ api.createGroup = {
|
||||
};
|
||||
|
||||
const analyticsObject = {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -211,6 +220,7 @@ api.createGroupPlan = {
|
||||
const savedGroup = results[1];
|
||||
|
||||
res.analytics.track('join group', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -322,7 +332,7 @@ api.getGroups = {
|
||||
const types = req.query.type.split(',');
|
||||
|
||||
const paginate = req.query.paginate === 'true';
|
||||
if (paginate && !_.includes(types, 'publicGuilds')) {
|
||||
if (paginate && !includes(types, 'publicGuilds')) {
|
||||
throw new BadRequest(apiError('guildsOnlyPaginate'));
|
||||
}
|
||||
|
||||
@@ -356,7 +366,7 @@ api.getGroups = {
|
||||
|
||||
if (req.query.search) {
|
||||
filters.$or = [];
|
||||
const searchWords = _.escapeRegExp(req.query.search.trim()).split(/\s+/).join('|');
|
||||
const searchWords = escapeRegExp(req.query.search.trim()).split(/\s+/).join('|');
|
||||
const searchQuery = { $regex: new RegExp(`${searchWords}`, 'i') };
|
||||
filters.$or.push({ name: searchQuery });
|
||||
filters.$or.push({ summary: searchQuery });
|
||||
@@ -497,16 +507,16 @@ api.updateGroup = {
|
||||
}
|
||||
|
||||
const handleArrays = (currentValue, updatedValue) => {
|
||||
if (!_.isArray(currentValue)) {
|
||||
if (!isArray(currentValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Previously, categories could get duplicated. By making the updated category list unique,
|
||||
// the duplication issue is fixed on every group edit
|
||||
return _.uniqBy(updatedValue, 'slug');
|
||||
return uniqBy(updatedValue, 'slug');
|
||||
};
|
||||
|
||||
_.assign(group, _.mergeWith(group.toObject(), Group.sanitizeUpdate(req.body), handleArrays));
|
||||
assign(group, mergeWith(group.toObject(), Group.sanitizeUpdate(req.body), handleArrays));
|
||||
|
||||
const savedGroup = await group.save();
|
||||
const response = await Group.toJSONCleanChat(savedGroup, user);
|
||||
@@ -573,7 +583,7 @@ api.joinGroup = {
|
||||
|
||||
if (group.type === 'party') {
|
||||
// Check if was invited to party
|
||||
const inviterParty = _.find(user.invitations.parties, { id: group._id });
|
||||
const inviterParty = find(user.invitations.parties, { id: group._id });
|
||||
if (inviterParty) {
|
||||
// Check if the user is already a member of the party or not. Only make the user leave the
|
||||
// party if the user is not a member of the party. See #12291 for more details.
|
||||
@@ -695,6 +705,7 @@ api.joinGroup = {
|
||||
promises.push(group.save());
|
||||
|
||||
const analyticsObject = {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -920,9 +931,9 @@ api.removeGroupMember = {
|
||||
}
|
||||
|
||||
let isInvited;
|
||||
if (_.find(member.invitations.parties, { id: group._id })) {
|
||||
if (find(member.invitations.parties, { id: group._id })) {
|
||||
isInvited = 'party';
|
||||
} else if (_.findIndex(member.invitations.guilds, { id: group._id }) !== -1) {
|
||||
} else if (findIndex(member.invitations.guilds, { id: group._id }) !== -1) {
|
||||
isInvited = 'guild';
|
||||
}
|
||||
|
||||
|
||||
@@ -479,7 +479,7 @@ api.updateHero = {
|
||||
}
|
||||
|
||||
if (updateData.auth.local && updateData.auth.local.email) {
|
||||
hero.auth.local.email = updateData.auth.local.email;
|
||||
hero.auth.local.email = updateData.auth.local.email.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import pick from 'lodash/pick';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import {
|
||||
model as User,
|
||||
@@ -736,6 +737,7 @@ api.transferGems = {
|
||||
|
||||
if (res.analytics) {
|
||||
res.analytics.track('transfer gems', {
|
||||
user: pick(sender, ['preferences', 'registeredThrough']),
|
||||
uuid: sender._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import each from 'lodash/each';
|
||||
import every from 'lodash/every';
|
||||
import isBoolean from 'lodash/isBoolean';
|
||||
import pick from 'lodash/pick';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
|
||||
import {
|
||||
@@ -28,7 +31,7 @@ const questScrolls = common.content.quests;
|
||||
function canStartQuestAutomatically (group) {
|
||||
// If all members are either true (accepted) or false (rejected) return true
|
||||
// If any member is null/undefined (undecided) return false
|
||||
return _.every(group.quest.members, _.isBoolean);
|
||||
return every(group.quest.members, isBoolean);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +107,7 @@ api.inviteToQuest = {
|
||||
},
|
||||
}).exec();
|
||||
|
||||
_.each(members, member => {
|
||||
each(members, member => {
|
||||
group.quest.members[member._id] = null;
|
||||
});
|
||||
|
||||
@@ -165,13 +168,13 @@ api.inviteToQuest = {
|
||||
|
||||
// track that the inviting user has accepted the quest
|
||||
analytics.track('quest', {
|
||||
category: 'behavior',
|
||||
owner: true,
|
||||
response: 'accept',
|
||||
gaLabel: 'accept',
|
||||
questName: questKey,
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
owner: true,
|
||||
questName: questKey,
|
||||
response: 'accept',
|
||||
});
|
||||
|
||||
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
|
||||
@@ -230,10 +233,10 @@ api.acceptQuest = {
|
||||
|
||||
// track that a user has accepted the quest
|
||||
analytics.track('quest', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
category: 'behavior',
|
||||
owner: false,
|
||||
response: 'accept',
|
||||
gaLabel: 'accept',
|
||||
questName: group.quest.key,
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
@@ -295,10 +298,10 @@ api.rejectQuest = {
|
||||
res.respond(200, savedGroup.quest);
|
||||
|
||||
analytics.track('quest', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
category: 'behavior',
|
||||
owner: false,
|
||||
response: 'reject',
|
||||
gaLabel: 'reject',
|
||||
questName: group.quest.key,
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
@@ -359,10 +362,10 @@ api.forceStart = {
|
||||
res.respond(200, savedGroup.quest);
|
||||
|
||||
analytics.track('quest', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
category: 'behavior',
|
||||
owner: user._id === group.quest.leader,
|
||||
response: 'force-start',
|
||||
gaLabel: 'force-start',
|
||||
questName: group.quest.key,
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import assign from 'lodash/assign';
|
||||
import find from 'lodash/find';
|
||||
import merge from 'lodash/merge';
|
||||
import pick from 'lodash/pick';
|
||||
import moment from 'moment';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import {
|
||||
@@ -330,6 +333,7 @@ api.createChallengeTasks = {
|
||||
|
||||
tasks.forEach(task => {
|
||||
res.analytics.track('challenge task created', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -645,7 +649,7 @@ api.updateTask = {
|
||||
sanitizedObj = Tasks.Task.sanitize(updatedTaskObj);
|
||||
}
|
||||
|
||||
_.assign(task, sanitizedObj);
|
||||
assign(task, sanitizedObj);
|
||||
|
||||
// console.log(task.modifiedPaths(), task.toObject().repeat === tep)
|
||||
// repeat is always among modifiedPaths because mongoose changes
|
||||
@@ -699,6 +703,7 @@ api.updateTask = {
|
||||
|
||||
if (group) {
|
||||
res.analytics.track('task edit', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -765,7 +770,7 @@ api.scoreTask = {
|
||||
|
||||
const userStats = user.stats.toJSON();
|
||||
|
||||
const resJsonData = _.assign({
|
||||
const resJsonData = assign({
|
||||
delta: taskResponse.delta,
|
||||
_tmp: user._tmp,
|
||||
}, userStats);
|
||||
@@ -970,7 +975,7 @@ api.scoreCheckListItem = {
|
||||
}
|
||||
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
||||
|
||||
const item = _.find(task.checklist, { id: req.params.itemId });
|
||||
const item = find(task.checklist, { id: req.params.itemId });
|
||||
|
||||
if (!item) throw new NotFound(res.t('checklistItemNotFound'));
|
||||
item.completed = !item.completed;
|
||||
@@ -1031,10 +1036,10 @@ api.updateChecklistItem = {
|
||||
verifyTaskModification(task, user, group, challenge, res);
|
||||
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
||||
|
||||
const item = _.find(task.checklist, { id: req.params.itemId });
|
||||
const item = find(task.checklist, { id: req.params.itemId });
|
||||
if (!item) throw new NotFound(res.t('checklistItemNotFound'));
|
||||
|
||||
_.merge(item, Tasks.Task.sanitizeChecklist(req.body));
|
||||
merge(item, Tasks.Task.sanitizeChecklist(req.body));
|
||||
const savedTask = await task.save();
|
||||
|
||||
res.respond(200, savedTask);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pick from 'lodash/pick';
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import * as Tasks from '../../../models/task';
|
||||
@@ -63,6 +64,7 @@ api.createGroupTasks = {
|
||||
|
||||
tasks.forEach(task => {
|
||||
res.analytics.track('team task created', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -251,6 +253,7 @@ api.assignTask = {
|
||||
res.respond(200, task);
|
||||
|
||||
res.analytics.track('task assign', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import forEach from 'lodash/forEach';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import pick from 'lodash/pick';
|
||||
import nconf from 'nconf';
|
||||
import get from 'lodash/get';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
@@ -124,13 +127,13 @@ api.getBuyList = {
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/inventory/buy',
|
||||
async handler (req, res) {
|
||||
const list = _.cloneDeep(common.updateStore(res.locals.user));
|
||||
const list = cloneDeep(common.updateStore(res.locals.user));
|
||||
|
||||
// return text and notes strings
|
||||
_.each(list, item => {
|
||||
_.each(item, (itemPropVal, itemPropKey) => {
|
||||
forEach(list, item => {
|
||||
forEach(item, (itemPropVal, itemPropKey) => {
|
||||
if (
|
||||
_.isFunction(itemPropVal)
|
||||
isFunction(itemPropVal)
|
||||
&& itemPropVal.i18nLangFunc
|
||||
) item[itemPropKey] = itemPropVal(req.language);
|
||||
});
|
||||
@@ -175,10 +178,10 @@ api.getInAppRewardsList = {
|
||||
const list = common.inAppRewards(res.locals.user);
|
||||
|
||||
// return text and notes strings
|
||||
_.each(list, item => {
|
||||
_.each(item, (itemPropVal, itemPropKey) => {
|
||||
forEach(list, item => {
|
||||
forEach(item, (itemPropVal, itemPropKey) => {
|
||||
if (
|
||||
_.isFunction(itemPropVal)
|
||||
isFunction(itemPropVal)
|
||||
&& itemPropVal.i18nLangFunc
|
||||
) item[itemPropKey] = itemPropVal(req.language);
|
||||
});
|
||||
@@ -323,6 +326,7 @@ api.deleteUser = {
|
||||
}
|
||||
|
||||
res.analytics.track('account delete', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -333,7 +337,7 @@ api.deleteUser = {
|
||||
};
|
||||
|
||||
function _cleanChecklist (task) {
|
||||
_.forEach(task.checklist, (c, i) => {
|
||||
forEach(task.checklist, (c, i) => {
|
||||
c.text = `item ${i}`;
|
||||
});
|
||||
}
|
||||
@@ -386,11 +390,11 @@ api.getUserAnonymized = {
|
||||
delete user.secret;
|
||||
delete user.permissions;
|
||||
|
||||
_.forEach(user.inbox.messages, msg => {
|
||||
forEach(user.inbox.messages, msg => {
|
||||
msg.text = 'inbox message text';
|
||||
});
|
||||
|
||||
_.forEach(user.tags, tag => {
|
||||
forEach(user.tags, tag => {
|
||||
tag.name = 'tag';
|
||||
tag.challenge = 'challenge';
|
||||
});
|
||||
@@ -404,7 +408,7 @@ api.getUserAnonymized = {
|
||||
};
|
||||
const tasks = await Tasks.Task.find(query).exec();
|
||||
|
||||
_.forEach(tasks, task => {
|
||||
forEach(tasks, task => {
|
||||
task.text = 'task text';
|
||||
task.notes = 'task notes';
|
||||
if (task.type === 'todo' || task.type === 'daily') {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
import * as authLib from '../../libs/auth';
|
||||
import { model as User } from '../../models/user';
|
||||
import { verifyUsername } from '../../libs/user/validation';
|
||||
import { isRestrictedEmailDomain } from '../../libs/auth/utils';
|
||||
|
||||
const api = {};
|
||||
|
||||
@@ -83,4 +84,49 @@ api.registerLocal = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {put} /api/v4/user/auth/check-email Check if email is used
|
||||
* @apiDescription Check if the email is already used by another user
|
||||
* @apiName CheckEmail
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam (Body) {String} email The checked email address.
|
||||
*
|
||||
* @apiSuccess {String} data.email The checked email address
|
||||
* @apiSuccess {Boolean} data.valid True if available, false if in use
|
||||
*/
|
||||
api.checkEmail = {
|
||||
method: 'POST',
|
||||
url: '/user/auth/check-email',
|
||||
async handler (req, res) {
|
||||
req.checkBody({
|
||||
email: {
|
||||
notEmpty: { errorMessage: res.t('missingEmail') },
|
||||
},
|
||||
});
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const lowercaseEmail = req.body.email.toLowerCase();
|
||||
if (isRestrictedEmailDomain(lowercaseEmail)) {
|
||||
return res.respond(200, {
|
||||
valid: false,
|
||||
email: req.body.email,
|
||||
error: res.t('cannotFulfillReq'),
|
||||
});
|
||||
}
|
||||
|
||||
const emailAlreadyInUse = await User.findOne({
|
||||
'auth.local.email': lowercaseEmail,
|
||||
}).select({ _id: 1 }).lean().exec();
|
||||
|
||||
if (emailAlreadyInUse) {
|
||||
return res.respond(200, { valid: false, email: req.body.email, error: res.t('cannotFulfillReq') });
|
||||
}
|
||||
|
||||
return res.respond(200, { valid: true, email: req.body.email });
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pick from 'lodash/pick';
|
||||
import {
|
||||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
@@ -19,7 +20,7 @@ api.trackEvent = {
|
||||
method: 'POST',
|
||||
url: '/analytics/track/:eventName',
|
||||
// we authenticate these requests to make sure they actually came from a real user
|
||||
middlewares: [authWithHeaders({ optional: true })],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
// As of now only web can track events using this route
|
||||
if (req.headers['x-client'] !== 'habitica-web') {
|
||||
@@ -30,11 +31,10 @@ api.trackEvent = {
|
||||
const eventProperties = req.body;
|
||||
|
||||
res.analytics.track(req.params.eventName, {
|
||||
uuid: user ? user._id : null,
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
headers: req.headers,
|
||||
category: 'behaviour',
|
||||
gaLabel: 'local',
|
||||
// hitType: 'event', sent from the client
|
||||
category: 'behavior',
|
||||
...eventProperties,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import Amplitude from 'amplitude';
|
||||
import googleAnalytics from 'universal-analytics';
|
||||
import useragent from 'useragent';
|
||||
import {
|
||||
each,
|
||||
omit,
|
||||
toArray,
|
||||
} from 'lodash';
|
||||
import common from '../../common';
|
||||
import logger from './logger';
|
||||
|
||||
const AMPLITUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
|
||||
const GA_TOKEN = nconf.get('GA_ID');
|
||||
|
||||
const LOG_AMPLITUDE_EVENTS = nconf.get('LOG_AMPLITUDE_EVENTS') === 'true';
|
||||
|
||||
const GA_POSSIBLE_LABELS = ['gaLabel', 'itemKey'];
|
||||
const GA_POSSIBLE_VALUES = ['gaValue', 'gemCost', 'goldCost'];
|
||||
const AMPLITUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
|
||||
const AMPLITUDE_PROPERTIES_TO_SCRUB = [
|
||||
'uuid', 'user', 'purchaseValue',
|
||||
'gaLabel', 'gaValue', 'headers', 'registeredThrough',
|
||||
'headers', 'registeredThrough',
|
||||
];
|
||||
|
||||
const PLATFORM_MAP = Object.freeze({
|
||||
@@ -32,8 +25,6 @@ const PLATFORM_MAP = Object.freeze({
|
||||
let amplitude;
|
||||
if (AMPLITUDE_TOKEN) amplitude = new Amplitude(AMPLITUDE_TOKEN);
|
||||
|
||||
const ga = googleAnalytics(GA_TOKEN);
|
||||
|
||||
const Content = common.content;
|
||||
|
||||
function _lookUpItemName (itemKey) {
|
||||
@@ -192,64 +183,6 @@ function _sendDataToAmplitude (eventType, data, loggerOnly) {
|
||||
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
|
||||
}
|
||||
|
||||
function _generateLabelForGoogleAnalytics (data) {
|
||||
let label;
|
||||
|
||||
each(GA_POSSIBLE_LABELS, key => {
|
||||
if (data[key]) {
|
||||
label = data[key];
|
||||
return false; // exit each early
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function _generateValueForGoogleAnalytics (data) {
|
||||
let value;
|
||||
|
||||
each(GA_POSSIBLE_VALUES, key => {
|
||||
if (data[key]) {
|
||||
value = data[key];
|
||||
return false; // exit each early
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function _sendDataToGoogle (eventType, data) {
|
||||
const eventData = {
|
||||
ec: data.gaCategory || data.category || 'behavior',
|
||||
ea: eventType,
|
||||
};
|
||||
|
||||
const label = _generateLabelForGoogleAnalytics(data);
|
||||
|
||||
if (label) {
|
||||
eventData.el = label;
|
||||
}
|
||||
|
||||
const value = _generateValueForGoogleAnalytics(data);
|
||||
|
||||
if (value) {
|
||||
eventData.ev = value;
|
||||
}
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
ga.event(eventData, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return promise.catch(err => logger.error(err, 'Error while sending data to Google Analytics.'));
|
||||
}
|
||||
|
||||
function _sendPurchaseDataToAmplitude (data) {
|
||||
const amplitudeData = _formatDataForAmplitude(data);
|
||||
|
||||
@@ -271,45 +204,6 @@ function _sendPurchaseDataToAmplitude (data) {
|
||||
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
|
||||
}
|
||||
|
||||
function _sendPurchaseDataToGoogle (data) {
|
||||
const label = data.paymentMethod;
|
||||
const type = data.purchaseType;
|
||||
const price = data.purchaseValue;
|
||||
const qty = data.quantity;
|
||||
const { sku } = data;
|
||||
const itemKey = data.itemPurchased;
|
||||
let variation = type;
|
||||
|
||||
if (data.gift) variation += ' - Gift';
|
||||
|
||||
const eventData = {
|
||||
ec: 'commerce',
|
||||
ea: type,
|
||||
el: label,
|
||||
ev: price,
|
||||
};
|
||||
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
ga.event(eventData, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const transactionPromise = new Promise((resolve, reject) => {
|
||||
ga.transaction(data.uuid, price)
|
||||
.item(price, qty, sku, itemKey, variation)
|
||||
.send(err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return Promise
|
||||
.all([eventPromise, transactionPromise])
|
||||
.catch(err => logger.error(err, 'Error while sending data to Google Analytics.'));
|
||||
}
|
||||
|
||||
function _setOnce (dataToSetOnce, uuid) {
|
||||
return amplitude
|
||||
.identify({
|
||||
@@ -323,14 +217,17 @@ function _setOnce (dataToSetOnce, uuid) {
|
||||
|
||||
// There's no error handling directly here because it's handled inside _sendDataTo{Amplitude|Google}
|
||||
async function track (eventType, data, loggerOnly = false) {
|
||||
const { user } = data;
|
||||
if (!user || !user.preferences || !user.preferences.analyticsConsent) {
|
||||
return null;
|
||||
}
|
||||
const promises = [
|
||||
_sendDataToAmplitude(eventType, data, loggerOnly),
|
||||
_sendDataToGoogle(eventType, data),
|
||||
];
|
||||
if (data.user && data.user.registeredThrough) {
|
||||
if (user.registeredThrough) {
|
||||
promises.push(_setOnce({
|
||||
registeredPlatform: data.user.registeredThrough,
|
||||
}, data.uuid || data.user._id));
|
||||
registeredPlatform: user.registeredThrough,
|
||||
}, data.uuid || user._id));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
@@ -339,9 +236,12 @@ async function track (eventType, data, loggerOnly = false) {
|
||||
// There's no error handling directly here because
|
||||
// it's handled inside _sendPurchaseDataTo{Amplitude|Google}
|
||||
async function trackPurchase (data) {
|
||||
const { user } = data;
|
||||
if (!user || !user.preferences || !user.preferences.analyticsConsent) {
|
||||
return null;
|
||||
}
|
||||
return Promise.all([
|
||||
_sendPurchaseDataToAmplitude(data),
|
||||
_sendPurchaseDataToGoogle(data),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -48,5 +48,6 @@ export async function appleProfile (req) {
|
||||
id: verifiedPayload.sub,
|
||||
emails: [{ value: verifiedPayload.email }],
|
||||
name: verifiedPayload.name || req.body.name || req.query.name,
|
||||
idToken,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import moment from 'moment';
|
||||
import pick from 'lodash/pick';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
@@ -218,12 +219,11 @@ async function registerLocal (req, res, { isV3 = false }) {
|
||||
|
||||
if (!existingUser) {
|
||||
res.analytics.track('register', {
|
||||
user: pick(savedUser, ['preferences', 'registeredThrough']),
|
||||
category: 'acquisition',
|
||||
type: 'local',
|
||||
gaLabel: 'local',
|
||||
uuid: savedUser._id,
|
||||
headers: req.headers,
|
||||
user: savedUser,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pick from 'lodash/pick';
|
||||
import passport from 'passport';
|
||||
import common from '../../../common';
|
||||
import { BadRequest, NotAuthorized } from '../errors';
|
||||
import { BadRequest, NotAuthorized, NotFound } from '../errors';
|
||||
import logger from '../logger';
|
||||
import {
|
||||
generateUsername,
|
||||
@@ -10,6 +11,7 @@ import { appleProfile } from './apple';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||
import { sendTxn as sendTxnEmail } from '../email';
|
||||
import { apiError } from '../apiError';
|
||||
|
||||
function _passportProfile (network, accessToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -33,14 +35,14 @@ export async function socialEmailToLocal (user) {
|
||||
{ 'auth.local.email': socialEmail },
|
||||
{ _id: 1 },
|
||||
).exec();
|
||||
if (!conflictingUser) return socialEmail;
|
||||
if (!conflictingUser) return socialEmail.toLowerCase();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function loginSocial (req, res) { // eslint-disable-line import/prefer-default-export
|
||||
let existingUser = res.locals.user;
|
||||
const { network } = req.body;
|
||||
const { network, allowRegister = true, username = generateUsername() } = req.body;
|
||||
|
||||
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS
|
||||
.find(supportedNetwork => supportedNetwork.key === network);
|
||||
@@ -83,6 +85,19 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
|
||||
existingUser = await User.findOne({ 'auth.local.email': email }).exec();
|
||||
}
|
||||
|
||||
if (!allowRegister && !existingUser) {
|
||||
if (network === 'apple') {
|
||||
return res.status(200).send({
|
||||
message: res.t('userNotFound'),
|
||||
id_token: profile.idToken,
|
||||
});
|
||||
}
|
||||
if (email) {
|
||||
throw new NotFound(`${apiError('socialFlowUserNotFound')} ${email}`);
|
||||
}
|
||||
throw new NotFound(res.t('userNotFound'));
|
||||
}
|
||||
|
||||
if (existingUser) {
|
||||
existingUser.auth[network] = {
|
||||
id: profile.id,
|
||||
@@ -90,8 +105,6 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
|
||||
};
|
||||
user = existingUser;
|
||||
} else {
|
||||
const generatedUsername = generateUsername();
|
||||
|
||||
user = {
|
||||
auth: {
|
||||
[network]: {
|
||||
@@ -99,8 +112,8 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
|
||||
emails: profile.emails,
|
||||
},
|
||||
local: {
|
||||
username: generatedUsername,
|
||||
lowerCaseUsername: generatedUsername,
|
||||
username,
|
||||
lowerCaseUsername: username.toLowerCase(),
|
||||
email,
|
||||
},
|
||||
},
|
||||
@@ -145,12 +158,11 @@ export async function loginSocial (req, res) { // eslint-disable-line import/pre
|
||||
|
||||
if (!existingUser) {
|
||||
res.analytics.track('register', {
|
||||
user: pick(savedUser, ['preferences', 'registeredThrough']),
|
||||
uuid: savedUser._id,
|
||||
category: 'acquisition',
|
||||
type: network,
|
||||
gaLabel: network,
|
||||
uuid: savedUser._id,
|
||||
headers: req.headers,
|
||||
user: savedUser,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import url from 'url';
|
||||
|
||||
import { NotAuthorized } from '../errors';
|
||||
|
||||
export const RESTRICTED_EMAIL_DOMAINS = Object.freeze(['habitica.com', 'habitrpg.com']);
|
||||
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL');
|
||||
const translator = shortid('0123456789abcdefghijklmnopqrstuvwxyz');
|
||||
|
||||
@@ -33,3 +34,9 @@ export function loginRes (user, req, res) {
|
||||
};
|
||||
return res.respond(200, responseData);
|
||||
}
|
||||
|
||||
export function isRestrictedEmailDomain (email) {
|
||||
if (!email) return false;
|
||||
const domain = email.toLowerCase().split('@')[1];
|
||||
return RESTRICTED_EMAIL_DOMAINS.includes(domain);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export async function bugReportLogic (user, userEmail, message, BROWSER_UA, ques
|
||||
USER_CUSTOMER_ID: user.purchased.plan.customerId,
|
||||
USER_CONSECUTIVE_MONTHS: user.purchased.plan.consecutive.count,
|
||||
USER_HOURGLASSES: user.purchased.plan.consecutive.trinkets,
|
||||
USER_ANALYTICS: user.preferences.analyticsConsent,
|
||||
REPORT_MSG: message,
|
||||
BROWSER_UA,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import mongoose from 'mongoose';
|
||||
import pick from 'lodash/pick';
|
||||
import nconf from 'nconf';
|
||||
import { model as User } from '../models/user';
|
||||
import * as Tasks from '../models/task';
|
||||
@@ -102,10 +103,8 @@ function processHabits (user, habits, now, daysMissed) {
|
||||
function trackCronAnalytics (analytics, user, _progress, options) {
|
||||
analytics.track('Cron', {
|
||||
category: 'behavior',
|
||||
gaLabel: 'Cron Count',
|
||||
gaValue: user.flags.cronCount,
|
||||
uuid: user._id,
|
||||
user,
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
resting: user.preferences.sleep,
|
||||
cronCount: user.flags.cronCount,
|
||||
progressUp: Math.min(_progress.up, 900),
|
||||
@@ -113,19 +112,6 @@ function trackCronAnalytics (analytics, user, _progress, options) {
|
||||
headers: options.headers,
|
||||
loginIncentives: user.loginIncentives,
|
||||
});
|
||||
|
||||
if (
|
||||
user.party && user.party.quest && !user.party.quest.RSVPNeeded
|
||||
&& !user.party.quest.completed && user.party.quest.key && !user.preferences.sleep
|
||||
) {
|
||||
analytics.track('quest participation', {
|
||||
category: 'behavior',
|
||||
uuid: user._id,
|
||||
user,
|
||||
questName: user.party.quest.key,
|
||||
headers: options.headers,
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
function awardLoginIncentives (user) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import find from 'lodash/find';
|
||||
import includes from 'lodash/includes';
|
||||
import pick from 'lodash/pick';
|
||||
|
||||
import { encrypt } from '../encryption';
|
||||
import { sendNotification as sendPushNotification } from '../pushNotifications';
|
||||
@@ -58,11 +60,11 @@ function sendInviteEmail (userToInvite, groupLabel, group, inviter) {
|
||||
function inviteUserToGuild (userToInvite, group, inviter, publicGuild, res) {
|
||||
const uuid = userToInvite._id;
|
||||
|
||||
if (_.includes(userToInvite.guilds, group._id)) {
|
||||
if (includes(userToInvite.guilds, group._id)) {
|
||||
throw new NotAuthorized(res.t('userAlreadyInGroup', { userId: uuid, username: userToInvite.profile.name }));
|
||||
}
|
||||
|
||||
if (_.find(userToInvite.invitations.guilds, { id: group._id })) {
|
||||
if (find(userToInvite.invitations.guilds, { id: group._id })) {
|
||||
throw new NotAuthorized(res.t('userAlreadyInvitedToGroup', { userId: uuid, username: userToInvite.profile.name }));
|
||||
}
|
||||
|
||||
@@ -82,7 +84,7 @@ async function inviteUserToParty (userToInvite, group, inviter, res) {
|
||||
const uuid = userToInvite._id;
|
||||
|
||||
// Do not add to invitations.parties array if the user is already invited to that party
|
||||
if (_.find(userToInvite.invitations.parties, { id: group._id })) {
|
||||
if (find(userToInvite.invitations.parties, { id: group._id })) {
|
||||
throw new NotAuthorized(res.t('userAlreadyPendingInvitation', { userId: uuid, username: userToInvite.profile.name }));
|
||||
}
|
||||
|
||||
@@ -142,9 +144,10 @@ async function inviteByUUID (uuid, group, inviter, req, res) {
|
||||
}
|
||||
|
||||
const analyticsObject = {
|
||||
user: pick(inviter, ['preferences', 'registeredThrough']),
|
||||
uuid: inviter._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
uuid: inviter._id,
|
||||
invitee: uuid,
|
||||
groupId: group._id,
|
||||
groupType: group.type,
|
||||
@@ -189,7 +192,7 @@ async function inviteByEmail (invite, group, inviter, req, res) {
|
||||
sentAt: Date.now(), // so we can let it expire
|
||||
cancelledPlan,
|
||||
});
|
||||
const link = `/static/home?groupInvite=${encrypt(groupQueryString)}`;
|
||||
const link = `/register?groupInvite=${encrypt(groupQueryString)}`;
|
||||
|
||||
const variables = [
|
||||
{ name: 'LINK', content: link },
|
||||
@@ -206,9 +209,10 @@ async function inviteByEmail (invite, group, inviter, req, res) {
|
||||
if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables);
|
||||
|
||||
const analyticsObject = {
|
||||
user: pick(inviter, ['preferences', 'registeredThrough']),
|
||||
uuid: inviter._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
uuid: inviter._id,
|
||||
invitee: 'email',
|
||||
groupId: group._id,
|
||||
groupType: group.type,
|
||||
@@ -243,9 +247,10 @@ async function inviteByUserName (username, group, inviter, req, res) {
|
||||
}
|
||||
|
||||
const analyticsObject = {
|
||||
user: pick(inviter, ['preferences', 'registeredThrough']),
|
||||
uuid: inviter._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
uuid: inviter._id,
|
||||
invitee: userToInvite._id,
|
||||
groupId: group._id,
|
||||
groupType: group.type,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import find from 'lodash/find';
|
||||
import pick from 'lodash/pick';
|
||||
import { getAnalyticsServiceByEnvironment } from '../analyticsService';
|
||||
import { getCurrentEventList } from '../worldState'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
@@ -114,6 +115,7 @@ export async function buyGems (data) {
|
||||
if (!data.gift) txnEmail(data.user, 'donation');
|
||||
|
||||
analytics.trackPurchase({
|
||||
user: pick(data.user, ['preferences', 'registeredThrough']),
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Gems',
|
||||
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pick from 'lodash/pick';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
BadRequest,
|
||||
@@ -31,6 +32,7 @@ async function buyGryphatrice (data) {
|
||||
data.user.purchased.txnCount += 1;
|
||||
|
||||
analytics.trackPurchase({
|
||||
user: pick(data.user, ['preferences', 'registeredThrough']),
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Gryphatrice',
|
||||
sku: `${data.paymentMethod.toLowerCase()}-checkout`,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// TODO these files need to refactored.
|
||||
|
||||
import _ from 'lodash';
|
||||
import defaults from 'lodash/defaults';
|
||||
import each from 'lodash/each';
|
||||
import find from 'lodash/find';
|
||||
import pick from 'lodash/pick';
|
||||
import moment from 'moment';
|
||||
|
||||
import { getAnalyticsServiceByEnvironment } from '../analyticsService';
|
||||
@@ -30,7 +33,7 @@ const analytics = getAnalyticsServiceByEnvironment();
|
||||
|
||||
function _findMysteryItems (user, dateMoment) {
|
||||
const pushedItems = [];
|
||||
_.each(shared.content.gear.flat, item => {
|
||||
each(shared.content.gear.flat, item => {
|
||||
if (
|
||||
item.klass === 'mystery'
|
||||
&& shared.content.mystery[item.mystery]
|
||||
@@ -200,7 +203,7 @@ async function prepareSubscriptionValues (data) {
|
||||
owner: data.user._id,
|
||||
});
|
||||
|
||||
_.defaults(plan, {
|
||||
defaults(plan, {
|
||||
gemsBought: 0,
|
||||
dateCreated: today,
|
||||
mysteryItems: [],
|
||||
@@ -339,7 +342,7 @@ async function createSubscription (data) {
|
||||
// Only send push notifications if sending to a user other than yourself
|
||||
if (data.gift.member._id !== data.user._id) {
|
||||
const currentEventList = getCurrentEventList();
|
||||
const currentEvent = _.find(currentEventList, event => Boolean(event.promo));
|
||||
const currentEvent = find(currentEventList, event => Boolean(event.promo));
|
||||
if (currentEvent && currentEvent.promo === 'g1g1') {
|
||||
const promoData = {
|
||||
user: data.user,
|
||||
@@ -462,9 +465,8 @@ async function cancelSubscription (data) {
|
||||
|
||||
analytics.track(cancelType, {
|
||||
uuid: data.user._id,
|
||||
user: pick(data.user, ['preferences', 'registeredThrough']),
|
||||
groupId,
|
||||
gaCategory: 'commerce',
|
||||
gaLabel: data.paymentMethod,
|
||||
paymentMethod: data.paymentMethod,
|
||||
headers: data.headers,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import compact from 'lodash/compact';
|
||||
import forEach from 'lodash/forEach';
|
||||
import keys from 'lodash/keys';
|
||||
import pick from 'lodash/pick';
|
||||
import remove from 'lodash/remove';
|
||||
import validator from 'validator';
|
||||
import {
|
||||
@@ -515,6 +516,7 @@ async function scoreTask (user, task, direction, req, res) {
|
||||
role = 'member';
|
||||
}
|
||||
res.analytics.track('team task scored', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
|
||||
@@ -117,6 +117,7 @@ export async function update (req, res, { isV3 = false }) {
|
||||
user.invitations.party = {};
|
||||
user.invitations.parties = [];
|
||||
res.analytics.track('Starts Looking for Party', {
|
||||
user: _.pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -200,6 +201,7 @@ export async function update (req, res, { isV3 = false }) {
|
||||
if (key === 'party.seeking' && val === null) {
|
||||
user.party.seeking = undefined;
|
||||
res.analytics.track('Leaves Looking for Party', {
|
||||
user: _.pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
@@ -290,6 +292,7 @@ export async function reset (req, res, { isV3 = false }) {
|
||||
]);
|
||||
|
||||
res.analytics.track('account reset', {
|
||||
user: _.pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
|
||||
@@ -4,6 +4,10 @@ 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';
|
||||
@@ -11,8 +15,6 @@ import { schema as UserNotificationSchema } from '../userNotification';
|
||||
import { schema as WebhookSchema } from '../webhook';
|
||||
import { model as Blocker } from '../blocker';
|
||||
|
||||
const RESTRICTED_EMAIL_DOMAINS = Object.freeze(['habitica.com', 'habitrpg.com']);
|
||||
|
||||
const BLOCKED_EMAILS = [];
|
||||
|
||||
Blocker.watchBlockers({
|
||||
@@ -60,9 +62,7 @@ export const UserSchema = new Schema({
|
||||
message: shared.i18n.t('invalidEmail'),
|
||||
}, {
|
||||
validator (email) {
|
||||
const lowercaseEmail = email.toLowerCase();
|
||||
|
||||
return RESTRICTED_EMAIL_DOMAINS.every(domain => !lowercaseEmail.endsWith(`@${domain}`));
|
||||
return !isRestrictedEmailDomain(email);
|
||||
},
|
||||
message: shared.i18n.t('invalidEmailDomain', { domains: RESTRICTED_EMAIL_DOMAINS.join(', ') }),
|
||||
}, {
|
||||
@@ -655,6 +655,7 @@ export const UserSchema = new Schema({
|
||||
return isValidCategory;
|
||||
},
|
||||
},
|
||||
analyticsConsent: Boolean,
|
||||
},
|
||||
profile: {
|
||||
blurb: String,
|
||||
|
||||
Reference in New Issue
Block a user