Compare commits
	
		
			137 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5fcf3fba88 | ||
|  | c88af182c9 | ||
|  | 8e559da200 | ||
|  | ed25afb0b1 | ||
|  | a2c91aae70 | ||
|  | 3ac69d5e75 | ||
|  | 87b26c4cfb | ||
|  | 28bc843779 | ||
|  | e92ff9737a | ||
|  | c3343c9412 | ||
|  | e9100c7132 | ||
|  | 11235685ca | ||
|  | 49d6691f7d | ||
|  | db4c4e6493 | ||
|  | 56b1f6371f | ||
|  | 1b5bd8e1ab | ||
|  | e39eafd3f0 | ||
|  | 9213181ca2 | ||
|  | 3e50469ed5 | ||
|  | 7e9d3062f3 | ||
|  | 6376e57614 | ||
|  | 84ad270436 | ||
|  | 92cf506bad | ||
|  | 5f97cb31b8 | ||
|  | 6d26fbc5f2 | ||
|  | bb9912de89 | ||
|  | 7a51b7593f | ||
|  | 1bfc55ece1 | ||
|  | e21f64edaf | ||
|  | a08b419411 | ||
|  | 4a752c3347 | ||
|  | 8609aae1b4 | ||
|  | d35fd9d90d | ||
|  | 11103813f5 | ||
|  | 1a75c6a696 | ||
|  | 6bef105cf6 | ||
|  | 3fffe7aa5c | ||
|  | 9ab9b0f553 | ||
|  | a00add46a7 | ||
|  | b9d8da44de | ||
|  | 947e8a1836 | ||
|  | 7bdc974704 | ||
|  | fe8780d49c | ||
|  | 2fc4d0f00c | ||
|  | 4300c7b1bf | ||
|  | 2cd0ed5973 | ||
|  | 6e8bdf4cdf | ||
|  | 0bac1102cc | ||
|  | 3e96e54ad8 | ||
|  | 3458d89c1d | ||
|  | 25e72ad907 | ||
|  | 5cf6a67a36 | ||
|  | 9dcce382a3 | ||
|  | f6484c872a | ||
|  | 249ba77c01 | ||
|  | 7ff590cd88 | ||
|  | f297fef89e | ||
|  | b037cb0722 | ||
|  | b8c58a7e4f | ||
|  | f973bf1038 | ||
|  | aaea985cf2 | ||
|  | 1d0e08419f | ||
|  | fd6244eb15 | ||
|  | f8aa756d52 | ||
|  | ae7df804cb | ||
|  | de37eb1bb2 | ||
|  | cf03261bbf | ||
|  | 3ec95ad821 | ||
|  | 57d11d5b20 | ||
|  | 039e7d40b8 | ||
|  | 4389a9b478 | ||
|  | 289032047c | ||
|  | 6f5515214a | ||
|  | fd2c4e3265 | ||
|  | dd91bada8f | ||
|  | d724933640 | ||
|  | e4b74bc347 | ||
|  | c609db09c1 | ||
|  | 55feebdf9e | ||
|  | d8a99647e7 | ||
|  | 353b4aed05 | ||
|  | 411f82202b | ||
|  | 5a5a6e4c5d | ||
|  | 914eee015e | ||
|  | bbbc06733b | ||
|  | 92c3a640ee | ||
|  | a301f817e9 | ||
|  | 519af8f1b6 | ||
|  | a71abea032 | ||
|  | 3c623b08c4 | ||
|  | ddfa3f8a91 | ||
|  | e2d1de0cf0 | ||
|  | 9281de1801 | ||
|  | 4960171565 | ||
|  | d063a57faa | ||
|  | 0960eaf571 | ||
|  | fd0ec41c53 | ||
|  | 1420e1c8d7 | ||
|  | c7c854664f | ||
|  | 68e5679340 | ||
|  | 32a9dda2c6 | ||
|  | 8b19c0ad69 | ||
|  | bb8bd8842d | ||
|  | 33e8b64df6 | ||
|  | 1411706963 | ||
|  | e94631a002 | ||
|  | 427251ed1d | ||
|  | 39bb60638f | ||
|  | 71e34e654c | ||
|  | f01aba15be | ||
|  | fd4e760c05 | ||
|  | b5ed65b164 | ||
|  | 4ee14e7c2a | ||
|  | f40fb510a9 | ||
|  | 86951916e8 | ||
|  | fde8e54783 | ||
|  | 276e882092 | ||
|  | 88bfed7efe | ||
|  | 0936c2ff86 | ||
|  | bdedf8f563 | ||
|  | 9220323483 | ||
|  | 80c93ad934 | ||
|  | 010da977a4 | ||
|  | b9bfb3f722 | ||
|  | effb66a089 | ||
|  | dbdb5f81a5 | ||
|  | 2062e78959 | ||
|  | 018c5edfdd | ||
|  | b0a3e58d66 | ||
|  | 99960da2eb | ||
|  | 8c293505c4 | ||
|  | 10ac99fc2e | ||
|  | 34c7d4e3b8 | ||
|  | 50d9a355b0 | ||
|  | 35b5285ce6 | ||
|  | 2001b27c26 | ||
|  | 7064b363e0 | 
| @@ -1,4 +1,6 @@ | ||||
| node_modules/** | ||||
| content_cache | ||||
| content_cache/** | ||||
| website/client/** | ||||
| test/** | ||||
| .git/** | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| FROM node:12 | ||||
|  | ||||
| ENV ADMIN_EMAIL admin@habitica.com | ||||
| ENV EMAILS_COMMUNITY_MANAGER_EMAIL admin@habitica.com | ||||
| ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e | ||||
| ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91 | ||||
| ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43 | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| Habitica  [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE) [](https://www.codetriage.com/habitrpg/habitica) | ||||
| =============== | ||||
|  | ||||
| [](https://greenkeeper.io/) | ||||
|  | ||||
| [Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor. | ||||
|  | ||||
| **We need more programmers!** Your assistance will be greatly appreciated. The wiki pages below and the additional pages they link to will tell you how to get started on contributing code and where you can go to seek further help or ask questions: | ||||
|   | ||||
| @@ -32,7 +32,7 @@ | ||||
|   "LOGGLY_SUBDOMAIN": "example-subdomain", | ||||
|   "LOGGLY_TOKEN": "example-token", | ||||
|   "MAINTENANCE_MODE": "false", | ||||
|   "NODE_DB_URI": "mongodb://localhost/habitrpg", | ||||
|   "NODE_DB_URI": "mongodb://localhost:27017/habitrpg", | ||||
|   "MONGODB_POOL_SIZE": "10", | ||||
|   "NODE_ENV": "development", | ||||
|   "PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin", | ||||
| @@ -70,9 +70,10 @@ | ||||
|   "SLACK_URL": "https://hooks.slack.com/services/some-url", | ||||
|   "STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "STRIPE_PUB_KEY": "22223333444455556666777788889999", | ||||
|   "TEST_DB_URI": "mongodb://localhost/habitrpg_test", | ||||
|   "TEST_DB_URI": "mongodb://localhost:27017/habitrpg_test", | ||||
|   "TRANSIFEX_SLACK_CHANNEL": "transifex", | ||||
|   "WEB_CONCURRENCY": 1, | ||||
|   "SKIP_SSL_CHECK_KEY": "key", | ||||
|   "ENABLE_STACKDRIVER_TRACING": "false" | ||||
|   "ENABLE_STACKDRIVER_TRACING": "false", | ||||
|   "BLOCKED_IPS": "" | ||||
| } | ||||
|   | ||||
| @@ -1,19 +1,20 @@ | ||||
| import gulp from 'gulp'; | ||||
| import babel from 'gulp-babel'; | ||||
|  | ||||
| gulp.task('build:src', () => gulp.src('website/server/**/*.js') | ||||
| gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js') | ||||
|   .pipe(babel()) | ||||
|   .pipe(gulp.dest('website/transpiled-babel/'))); | ||||
|  | ||||
| gulp.task('build:common', () => gulp.src('website/common/script/**/*.js') | ||||
| gulp.task('build:babel:common', () => gulp.src('website/common/script/**/*.js') | ||||
|   .pipe(babel()) | ||||
|   .pipe(gulp.dest('website/common/transpiled-babel/'))); | ||||
|  | ||||
| gulp.task('build:server', gulp.series('build:src', 'build:common', done => done())); | ||||
| gulp.task('build:babel', gulp.parallel('build:babel:server', 'build:babel:common', done => done())); | ||||
|  | ||||
| gulp.task('build:prod', gulp.series( | ||||
|   'build:server', | ||||
|   'build:babel', | ||||
|   'apidoc', | ||||
|   'content:cache', | ||||
|   done => done(), | ||||
| )); | ||||
|  | ||||
|   | ||||
							
								
								
									
										34
									
								
								gulp/gulp-content.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | ||||
| import gulp from 'gulp'; | ||||
| import fs from 'fs'; | ||||
|  | ||||
| // TODO parallelize, use gulp file helpers | ||||
| gulp.task('content:cache', done => { | ||||
|   // Requiring at runtime because these files access `common` | ||||
|   // code which in production works only if transpiled so after | ||||
|   // gulp build:babel:common has run | ||||
|   const { CONTENT_CACHE_PATH, getLocalizedContent } = require('../website/server/libs/content'); // eslint-disable-line global-require | ||||
|   const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require | ||||
|  | ||||
|   try { | ||||
|     // create the cache folder (if it doesn't exist) | ||||
|     try { | ||||
|       fs.mkdirSync(CONTENT_CACHE_PATH); | ||||
|     } catch (err) { | ||||
|       if (err.code !== 'EEXIST') throw err; | ||||
|     } | ||||
|  | ||||
|     // clone the content for each language and save | ||||
|     // localize it | ||||
|     // save the result | ||||
|     langCodes.forEach(langCode => { | ||||
|       fs.writeFileSync( | ||||
|         `${CONTENT_CACHE_PATH}${langCode}.json`, | ||||
|         getLocalizedContent(langCode), | ||||
|         'utf8', | ||||
|       ); | ||||
|     }); | ||||
|     done(); | ||||
|   } catch (err) { | ||||
|     done(err); | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										10
									
								
								gulpfile.js
									
									
									
									
									
								
							
							
						
						| @@ -13,8 +13,16 @@ const gulp = require('gulp'); | ||||
|  | ||||
| if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env | ||||
|   require('./gulp/gulp-apidoc'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-content'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-build'); // eslint-disable-line global-require | ||||
| } else { | ||||
|   require('glob').sync('./gulp/gulp-*').forEach(require); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-apidoc'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-content'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-build'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-console'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-sprites'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-start'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-tests'); // eslint-disable-line global-require | ||||
|   require('./gulp/gulp-transifex-test'); // eslint-disable-line global-require | ||||
|   require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require | ||||
| } | ||||
|   | ||||
							
								
								
									
										69
									
								
								migrations/archive/2020/20200402_webhooks_add_protocol.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,69 @@ | ||||
| /* eslint-disable no-console */ | ||||
| const MIGRATION_NAME = '20200402_webhooks_add_protocol'; | ||||
| import { model as User } from '../../../website/server/models/user'; | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| async function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const set = { | ||||
|     migration: MIGRATION_NAME, | ||||
|   }; | ||||
|  | ||||
|   if (user && user.webhooks && user.webhooks.length > 0) { | ||||
|     user.webhooks.forEach(webhook => { | ||||
|       // Make sure the protocol is set and valid | ||||
|       if (webhook.url.startsWith('ftp')) { | ||||
|         webhook.url = webhook.url.replace('ftp', 'https'); | ||||
|       } | ||||
|  | ||||
|       if (!webhook.url.startsWith('http://') && !webhook.url.startsWith('https://')) { | ||||
|         // the default in got 9 was https | ||||
|         // see https://github.com/sindresorhus/got/commit/92bc8082137d7d085750359bbd76c801e213d7d2#diff-0730bb7c2e8f9ea2438b52e419dd86c9L111 | ||||
|         webhook.url = `https://${webhook.url}`; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     set.webhooks = user.webhooks; | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   return await User.update({ _id: user._id }, { $set: set }).exec(); | ||||
| } | ||||
|  | ||||
| module.exports = async function processUsers () { | ||||
|   let query = { | ||||
|     migration: { $ne: MIGRATION_NAME }, | ||||
|     webhooks: { $exists: true, $not: { $size: 0 } }, | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|     webhooks: 1, | ||||
|   }; | ||||
|  | ||||
|   while (true) { // eslint-disable-line no-constant-condition | ||||
|     const users = await User // eslint-disable-line no-await-in-loop | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({_id: 1}) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       console.warn('All appropriate users found and modified.'); | ||||
|       console.warn(`\n${count} users processed\n`); | ||||
|       break; | ||||
|     } else { | ||||
|       query._id = { | ||||
|         $gt: users[users.length - 1]._id, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										63
									
								
								migrations/archive/2020/20200402_webhooks_reenable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,63 @@ | ||||
| /* eslint-disable no-console */ | ||||
| const MIGRATION_NAME = '20200402_webhooks_reenable'; | ||||
| import { model as User } from '../../../website/server/models/user'; | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| async function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const set = { | ||||
|     migration: MIGRATION_NAME, | ||||
|   }; | ||||
|  | ||||
|   if (user && user.webhooks && user.webhooks.length > 0) { | ||||
|     user.webhooks.forEach(webhook => { | ||||
|       // Re-enable webhooks disabled because of too many failures | ||||
|       if (webhook.enabled === false && webhook.lastFailureAt === null) { | ||||
|         webhook.enabled = true; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     set.webhooks = user.webhooks; | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   return await User.update({ _id: user._id }, { $set: set }).exec(); | ||||
| } | ||||
|  | ||||
| module.exports = async function processUsers () { | ||||
|   let query = { | ||||
|     migration: { $ne: MIGRATION_NAME }, | ||||
|     webhooks: { $exists: true, $not: { $size: 0 } }, | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|     webhooks: 1, | ||||
|   }; | ||||
|  | ||||
|   while (true) { // eslint-disable-line no-constant-condition | ||||
|     const users = await User // eslint-disable-line no-await-in-loop | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({_id: 1}) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       console.warn('All appropriate users found and modified.'); | ||||
|       console.warn(`\n${count} users processed\n`); | ||||
|       break; | ||||
|     } else { | ||||
|       query._id = { | ||||
|         $gt: users[users.length - 1]._id, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop | ||||
|   } | ||||
| }; | ||||
| @@ -18,7 +18,7 @@ function setUpServer () { | ||||
| setUpServer(); | ||||
|  | ||||
| // Replace this with your migration | ||||
| const processUsers = () => {}; // require('').default; | ||||
| const processUsers = require().default; | ||||
|  | ||||
| processUsers() | ||||
|   .then(() => { | ||||
|   | ||||
							
								
								
									
										54
									
								
								migrations/tasks/rewards-flip-negative-costs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | ||||
| // @migrationName = 'RewardsMigrationFlipNegativeCostsValues'; | ||||
| // @authorName = 'hamboomger'; | ||||
| // @authorUuid = '80b61b73-2278-4947-b713-a10112cfe7f5'; | ||||
|  | ||||
| /* | ||||
|  * For each reward with negative cost, make it positive | ||||
|  * by assigning it an absolute value of itself | ||||
|  */ | ||||
|  | ||||
| import { Task } from '../../website/server/models/task'; | ||||
|  | ||||
| async function flipNegativeCostsValues () { | ||||
|   const query = { | ||||
|     type: 'reward', | ||||
|     value: { $lt: 0 }, | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|     value: 1, | ||||
|   }; | ||||
|  | ||||
|   // eslint-disable-next-line no-constant-condition | ||||
|   while (true) { | ||||
|     // eslint-disable-next-line no-await-in-loop | ||||
|     const rewards = await Task | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({ _id: 1 }) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (rewards.length === 0) { | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     const promises = rewards.map(reward => { | ||||
|       const positiveValue = Math.abs(reward.value); | ||||
|       return Task.update({ _id: reward._id }, { $set: { value: positiveValue } }).exec(); | ||||
|     }); | ||||
|  | ||||
|     // eslint-disable-next-line no-await-in-loop | ||||
|     await Promise.all(promises); | ||||
|  | ||||
|     query._id = { | ||||
|       $gt: rewards[rewards.length - 1]._id, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   console.log('All rewards with negative values were updated, migration finished'); | ||||
| } | ||||
|  | ||||
| export default flipNegativeCostsValues; | ||||
| @@ -33,6 +33,9 @@ async function updateUser (user) { | ||||
|   each(keys(content.specialPets), pet => { | ||||
|     set[`items.pets.${pet}`] = 5; | ||||
|   }); | ||||
|   each(keys(content.wackyPets), pet => { | ||||
|     set[`items.pets.${pet}`] = 5; | ||||
|   }); | ||||
|   each(keys(content.mounts), mount => { | ||||
|     set[`items.mounts.${mount}`] = true; | ||||
|   }); | ||||
| @@ -54,7 +57,7 @@ async function updateUser (user) { | ||||
| export default async function processUsers () { | ||||
|   const query = { | ||||
|     migration: { $ne: MIGRATION_NAME }, | ||||
|     'auth.local.username': 'olson22', | ||||
|     'auth.local.username': 'SabreTest', | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| import { model as User } from '../../website/server/models/user'; | ||||
|  | ||||
| const MIGRATION_NAME = '20190314_pi_day'; | ||||
| const MIGRATION_NAME = '20200314_pi_day'; | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
| @@ -24,27 +24,37 @@ async function updateUser (user) { | ||||
|     'items.food.Pie_Red': 1, | ||||
|   }; | ||||
|   const set = {}; | ||||
|   let push; | ||||
|  | ||||
|   set.migration = MIGRATION_NAME; | ||||
|  | ||||
|   set['items.gear.owned.head_special_piDay'] = false; | ||||
|   set['items.gear.owned.shield_special_piDay'] = false; | ||||
|   const push = [ | ||||
|     { type: 'marketGear', path: 'gear.flat.head_special_piDay', _id: uuid() }, | ||||
|     { type: 'marketGear', path: 'gear.flat.shield_special_piDay', _id: uuid() }, | ||||
|   ]; | ||||
|   if (typeof user.items.gear.owned.head_special_piDay !== 'undefined') { | ||||
|     push = false; | ||||
|   } else { | ||||
|     set['items.gear.owned.head_special_piDay'] = false; | ||||
|     set['items.gear.owned.shield_special_piDay'] = false; | ||||
|     push = [ | ||||
|       { type: 'marketGear', path: 'gear.flat.head_special_piDay', _id: uuid() }, | ||||
|       { type: 'marketGear', path: 'gear.flat.shield_special_piDay', _id: uuid() }, | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   if (push) { | ||||
|     return User | ||||
|       .update({ _id: user._id }, { $inc: inc, $set: set, $push: { pinnedItems: { $each: push } } }) | ||||
|       .exec(); | ||||
|   } | ||||
|   return User | ||||
|     .update({ _id: user._id }, { $inc: inc, $set: set, $push: { pinnedItems: { $each: push } } }) | ||||
|     .update({ _id: user._id }, { $inc: inc, $set: set }) | ||||
|     .exec(); | ||||
| } | ||||
|  | ||||
| export default async function processUsers () { | ||||
|   const query = { | ||||
|     migration: { $ne: MIGRATION_NAME }, | ||||
|     'auth.timestamps.loggedin': { $gt: new Date('2019-02-15') }, | ||||
|     'auth.timestamps.loggedin': { $gt: new Date('2020-02-15') }, | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|   | ||||
							
								
								
									
										1856
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										25
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,20 +1,20 @@ | ||||
| { | ||||
|   "name": "habitica", | ||||
|   "description": "A habit tracker app which treats your goals like a Role Playing Game.", | ||||
|   "version": "4.136.1", | ||||
|   "version": "4.138.6", | ||||
|   "main": "./website/server/index.js", | ||||
|   "dependencies": { | ||||
|     "@babel/core": "^7.8.6", | ||||
|     "@babel/preset-env": "^7.8.6", | ||||
|     "@babel/register": "^7.8.6", | ||||
|     "@babel/core": "^7.9.0", | ||||
|     "@babel/preset-env": "^7.9.0", | ||||
|     "@babel/register": "^7.9.0", | ||||
|     "@google-cloud/trace-agent": "^4.2.5", | ||||
|     "@slack/client": "^3.8.1", | ||||
|     "@slack/client": "^4.12.0", | ||||
|     "accepts": "^1.3.5", | ||||
|     "amazon-payments": "^0.2.8", | ||||
|     "amplitude": "^3.5.0", | ||||
|     "apidoc": "^0.17.5", | ||||
|     "apn": "^2.2.0", | ||||
|     "aws-sdk": "^2.630.0", | ||||
|     "aws-sdk": "^2.648.0", | ||||
|     "bcrypt": "^3.0.8", | ||||
|     "body-parser": "^1.18.3", | ||||
|     "compression": "^1.7.4", | ||||
| @@ -30,14 +30,14 @@ | ||||
|     "express-basic-auth": "^1.1.5", | ||||
|     "express-validator": "^5.2.0", | ||||
|     "glob": "^7.1.6", | ||||
|     "got": "^9.0.0", | ||||
|     "got": "^10.7.0", | ||||
|     "gulp": "^4.0.0", | ||||
|     "gulp-babel": "^8.0.0", | ||||
|     "gulp-imagemin": "^6.2.0", | ||||
|     "gulp-nodemon": "^2.4.1", | ||||
|     "gulp-nodemon": "^2.5.0", | ||||
|     "gulp.spritesmith": "^6.9.0", | ||||
|     "habitica-markdown": "^1.3.2", | ||||
|     "helmet": "^3.21.3", | ||||
|     "helmet": "^3.22.0", | ||||
|     "image-size": "^0.8.3", | ||||
|     "in-app-purchase": "^1.11.3", | ||||
|     "js2xmlparser": "^4.0.1", | ||||
| @@ -46,11 +46,10 @@ | ||||
|     "method-override": "^3.0.0", | ||||
|     "moment": "^2.24.0", | ||||
|     "moment-recur": "^1.0.7", | ||||
|     "mongoose": "^5.9.2", | ||||
|     "morgan": "^1.7.0", | ||||
|     "mongoose": "^5.9.6", | ||||
|     "morgan": "^1.10.0", | ||||
|     "nconf": "^0.10.0", | ||||
|     "node-gcm": "^1.0.2", | ||||
|     "pageres": "^5.1.0", | ||||
|     "passport": "^0.4.1", | ||||
|     "passport-facebook": "^3.0.0", | ||||
|     "passport-google-oauth2": "^0.2.0", | ||||
| @@ -58,7 +57,7 @@ | ||||
|     "paypal-ipn": "3.0.0", | ||||
|     "paypal-rest-sdk": "^1.8.1", | ||||
|     "ps-tree": "^1.0.0", | ||||
|     "regenerator-runtime": "^0.13.3", | ||||
|     "regenerator-runtime": "^0.13.5", | ||||
|     "remove-markdown": "^0.3.0", | ||||
|     "rimraf": "^3.0.2", | ||||
|     "short-uuid": "^3.0.0", | ||||
|   | ||||
							
								
								
									
										17
									
								
								test/api/unit/libs/content.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| import * as contentLib from '../../../../website/server/libs/content'; | ||||
| import content from '../../../../website/common/script/content'; | ||||
|  | ||||
| describe('contentLib', () => { | ||||
|   describe('CONTENT_CACHE_PATH', () => { | ||||
|     it('exports CONTENT_CACHE_PATH', () => { | ||||
|       expect(contentLib.CONTENT_CACHE_PATH).to.be.a.string; | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getLocalizedContent', () => { | ||||
|     it('clones, not modify, the original content data', () => { | ||||
|       contentLib.getLocalizedContent(); | ||||
|       expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -121,8 +121,7 @@ describe('emails', () => { | ||||
|  | ||||
|       sendTxnEmail(mailingInfo, emailType); | ||||
|       expect(got.post).to.be.calledWith('undefined/job', sinon.match({ | ||||
|         json: true, | ||||
|         body: { | ||||
|         json: { | ||||
|           data: { | ||||
|             emailType: sinon.match.same(emailType), | ||||
|             to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'), | ||||
| @@ -154,8 +153,7 @@ describe('emails', () => { | ||||
|  | ||||
|       sendTxnEmail(mailingInfo, emailType); | ||||
|       expect(got.post).to.be.calledWith('undefined/job', sinon.match({ | ||||
|         json: true, | ||||
|         body: { | ||||
|         json: { | ||||
|           data: { | ||||
|             emailType: sinon.match.same(emailType), | ||||
|             to: sinon.match(val => val[0]._id === mailingInfo._id), | ||||
| @@ -177,8 +175,7 @@ describe('emails', () => { | ||||
|  | ||||
|       sendTxnEmail(mailingInfo, emailType, variables); | ||||
|       expect(got.post).to.be.calledWith('undefined/job', sinon.match({ | ||||
|         json: true, | ||||
|         body: { | ||||
|         json: { | ||||
|           data: { | ||||
|             variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'), | ||||
|             personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { | ||||
|   CustomError, | ||||
|   NotAuthorized, | ||||
|   BadRequest, | ||||
|   Forbidden, | ||||
|   InternalServerError, | ||||
|   NotFound, | ||||
|   NotificationNotFound, | ||||
| @@ -113,6 +114,32 @@ describe('Custom Errors', () => { | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('Forbidden', () => { | ||||
|     it('is an instance of CustomError', () => { | ||||
|       const forbiddenError = new Forbidden(); | ||||
|  | ||||
|       expect(forbiddenError).to.be.an.instanceOf(CustomError); | ||||
|     }); | ||||
|  | ||||
|     it('it returns an http code of 401', () => { | ||||
|       const forbiddenError = new Forbidden(); | ||||
|  | ||||
|       expect(forbiddenError.httpCode).to.eql(403); | ||||
|     }); | ||||
|  | ||||
|     it('returns a default message', () => { | ||||
|       const forbiddenError = new Forbidden(); | ||||
|  | ||||
|       expect(forbiddenError.message).to.eql('Access forbidden.'); | ||||
|     }); | ||||
|  | ||||
|     it('allows a custom message', () => { | ||||
|       const forbiddenError = new Forbidden('Custom Error Message'); | ||||
|  | ||||
|       expect(forbiddenError.message).to.eql('Custom Error Message'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('InternalServerError', () => { | ||||
|     it('is an instance of CustomError', () => { | ||||
|       const internalServerError = new InternalServerError(); | ||||
|   | ||||
							
								
								
									
										111
									
								
								test/api/unit/libs/language.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,111 @@ | ||||
| import { | ||||
|   getLanguageFromBrowser, | ||||
|   getLanguageFromUser, | ||||
| } from '../../../../website/server/libs/language'; | ||||
| import { | ||||
|   generateReq, | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
|  | ||||
| describe('language lib', () => { | ||||
|   let req; | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     req = generateReq(); | ||||
|   }); | ||||
|  | ||||
|   describe('getLanguageFromUser', () => { | ||||
|     it('uses the user preferred language if avalaible', () => { | ||||
|       const user = { | ||||
|         preferences: { | ||||
|           language: 'it', | ||||
|         }, | ||||
|       }; | ||||
|  | ||||
|       expect(getLanguageFromUser(user, req)).to.equal('it'); | ||||
|     }); | ||||
|  | ||||
|     it('falls back to english if the user preferred language is not avalaible', () => { | ||||
|       const user = { | ||||
|         preferences: { | ||||
|           language: 'bla', | ||||
|         }, | ||||
|       }; | ||||
|  | ||||
|       expect(getLanguageFromUser(user, req)).to.equal('en'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getLanguageFromBrowser', () => { | ||||
|     it('uses browser specificed language', () => { | ||||
|       req.headers['accept-language'] = 'pt'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('pt'); | ||||
|     }); | ||||
|  | ||||
|     it('uses first language in series if browser specifies multiple', () => { | ||||
|       req.headers['accept-language'] = 'he, pt, it'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('he'); | ||||
|     }); | ||||
|  | ||||
|     it('skips invalid lanaguages and uses first language in series if browser specifies multiple', () => { | ||||
|       req.headers['accept-language'] = 'blah, he, pt, it'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('he'); | ||||
|     }); | ||||
|  | ||||
|     it('uses normal version of language if specialized locale is passed in', () => { | ||||
|       req.headers['accept-language'] = 'fr-CA'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('fr'); | ||||
|     }); | ||||
|  | ||||
|     it('uses normal version of language if specialized locale is passed in', () => { | ||||
|       req.headers['accept-language'] = 'fr-CA'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('fr'); | ||||
|     }); | ||||
|  | ||||
|     it('uses es if es is passed in', () => { | ||||
|       req.headers['accept-language'] = 'es'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('es'); | ||||
|     }); | ||||
|  | ||||
|     it('uses es_419 if applicable es-languages are passed in', () => { | ||||
|       req.headers['accept-language'] = 'es-mx'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('es_419'); | ||||
|     }); | ||||
|  | ||||
|     it('uses es_419 if multiple es languages are passed in', () => { | ||||
|       req.headers['accept-language'] = 'es-GT, es-MX, es-CR'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('es_419'); | ||||
|     }); | ||||
|  | ||||
|     it('zh', () => { | ||||
|       req.headers['accept-language'] = 'zh-TW'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('zh_TW'); | ||||
|     }); | ||||
|  | ||||
|     it('uses english if browser specified language is not compatible', () => { | ||||
|       req.headers['accept-language'] = 'blah'; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('en'); | ||||
|     }); | ||||
|  | ||||
|     it('uses english if browser does not specify', () => { | ||||
|       req.headers['accept-language'] = ''; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('en'); | ||||
|     }); | ||||
|  | ||||
|     it('uses english if browser does not supply an accept-language header', () => { | ||||
|       delete req.headers['accept-language']; | ||||
|  | ||||
|       expect(getLanguageFromBrowser(req)).to.equal('en'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -30,18 +30,52 @@ describe('logger', () => { | ||||
|  | ||||
|   describe('info', () => { | ||||
|     it('calls winston\'s info log', () => { | ||||
|       logger.info(1, 2, 3); | ||||
|       logger.info('1', 2); | ||||
|       expect(infoSpy).to.be.calledOnce; | ||||
|       expect(infoSpy).to.be.calledWith(1, 2, 3); | ||||
|       expect(infoSpy).to.be.calledWith('1', { extraData: 2 }); | ||||
|     }); | ||||
|  | ||||
|     it('allows up to two arguments', () => { | ||||
|       expect(() => logger.info('1', 2, 3)).to.throw; | ||||
|       expect(infoSpy).to.not.be.called; | ||||
|     }); | ||||
|  | ||||
|     it('has default message', () => { | ||||
|       logger.info(1); | ||||
|       expect(infoSpy).to.be.calledOnce; | ||||
|       expect(infoSpy).to.be.calledWith('No message provided for log.', { extraData: 1 }); | ||||
|     }); | ||||
|  | ||||
|     it('wraps non objects', () => { | ||||
|       logger.info('message', [1, 2]); | ||||
|       expect(infoSpy).to.be.calledOnce; | ||||
|       expect(infoSpy).to.be.calledWithMatch('message', { extraData: [1, 2] }); | ||||
|     }); | ||||
|  | ||||
|     it('does not wrap objects', () => { | ||||
|       logger.info('message', { a: 1, b: 2 }); | ||||
|       expect(infoSpy).to.be.calledOnce; | ||||
|       expect(infoSpy).to.be.calledWithMatch('message', { a: 1, b: 2 }); | ||||
|     }); | ||||
|  | ||||
|     it('throws if two arguments and no message', () => { | ||||
|       expect(() => logger.info({ a: 1 }, { b: 2 })).to.throw; | ||||
|       expect(infoSpy).to.not.be.called; | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('error', () => { | ||||
|     context('non-error object', () => { | ||||
|       it('passes through arguments if the first arg is not an error object', () => { | ||||
|         logger.error(1, 2, 3, 4); | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWith(1, 2, 3, 4); | ||||
|     it('allows up to two arguments', () => { | ||||
|       expect(() => logger.error('1', 2, 3)).to.throw; | ||||
|       expect(errorSpy).to.not.be.called; | ||||
|     }); | ||||
|  | ||||
|     it('handled non-error object', () => { | ||||
|       logger.error(1, 2); | ||||
|       expect(errorSpy).to.be.calledOnce; | ||||
|       expect(errorSpy).to.be.calledWithMatch('logger.error expects an Error instance', { | ||||
|         invalidErr: 1, | ||||
|         extraData: 2, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
| @@ -50,14 +84,12 @@ describe('logger', () => { | ||||
|         const errInstance = new Error('An error.'); | ||||
|         logger.error(errInstance, { | ||||
|           data: 1, | ||||
|         }, 2, 3); | ||||
|         }); | ||||
|  | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWith( | ||||
|           errInstance.stack, | ||||
|           { data: 1, fullError: errInstance }, | ||||
|           2, | ||||
|           3, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
| @@ -68,56 +100,60 @@ describe('logger', () => { | ||||
|         logger.error(errInstance, { | ||||
|           data: 1, | ||||
|           fullError: anotherError, | ||||
|         }, 2, 3); | ||||
|         }); | ||||
|  | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWith( | ||||
|           errInstance.stack, | ||||
|           { data: 1, fullError: anotherError }, | ||||
|           2, | ||||
|           3, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('logs the error when errorData is null', () => { | ||||
|         const errInstance = new Error('An error.'); | ||||
|  | ||||
|         logger.error(errInstance, null, 2, 3); | ||||
|         logger.error(errInstance, null); | ||||
|  | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWith( | ||||
|         expect(errorSpy).to.be.calledWithMatch( | ||||
|           errInstance.stack, | ||||
|           null, | ||||
|           2, | ||||
|           3, | ||||
|           { }, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('logs the error when errorData is not an object', () => { | ||||
|         const errInstance = new Error('An error.'); | ||||
|  | ||||
|         logger.error(errInstance, true, 2, 3); | ||||
|         logger.error(errInstance, true); | ||||
|  | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWith( | ||||
|         expect(errorSpy).to.be.calledWithMatch( | ||||
|           errInstance.stack, | ||||
|           true, | ||||
|           2, | ||||
|           3, | ||||
|           { extraData: true }, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('logs the error when errorData is a string', () => { | ||||
|         const errInstance = new Error('An error.'); | ||||
|  | ||||
|         logger.error(errInstance, 'a string'); | ||||
|  | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWithMatch( | ||||
|           errInstance.stack, | ||||
|           { extraMessage: 'a string' }, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       it('logs the error when errorData does not include isHandledError property', () => { | ||||
|         const errInstance = new Error('An error.'); | ||||
|  | ||||
|         logger.error(errInstance, { httpCode: 400 }, 2, 3); | ||||
|         logger.error(errInstance, { httpCode: 400 }); | ||||
|  | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWith( | ||||
|           errInstance.stack, | ||||
|           { httpCode: 400, fullError: errInstance }, | ||||
|           2, | ||||
|           3, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
| @@ -127,14 +163,12 @@ describe('logger', () => { | ||||
|         logger.error(errInstance, { | ||||
|           isHandledError: true, | ||||
|           httpCode: 502, | ||||
|         }, 2, 3); | ||||
|         }); | ||||
|  | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWith( | ||||
|           errInstance.stack, | ||||
|           { httpCode: 502, isHandledError: true, fullError: errInstance }, | ||||
|           2, | ||||
|           3, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
| @@ -144,14 +178,12 @@ describe('logger', () => { | ||||
|         logger.error(errInstance, { | ||||
|           isHandledError: true, | ||||
|           httpCode: 403, | ||||
|         }, 2, 3); | ||||
|         }); | ||||
|  | ||||
|         expect(warnSpy).to.be.calledOnce; | ||||
|         expect(warnSpy).to.be.calledWith( | ||||
|           errInstance.stack, | ||||
|           { httpCode: 403, isHandledError: true, fullError: errInstance }, | ||||
|           2, | ||||
|           3, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
| @@ -160,7 +192,7 @@ describe('logger', () => { | ||||
|  | ||||
|         errInstance.customField = 'Some interesting data'; | ||||
|  | ||||
|         logger.error(errInstance, {}, 2, 3); | ||||
|         logger.error(errInstance, {}); | ||||
|  | ||||
|         expect(errorSpy).to.be.calledOnce; | ||||
|         expect(errorSpy).to.be.calledWith( | ||||
| @@ -170,8 +202,6 @@ describe('logger', () => { | ||||
|               customField: 'Some interesting data', | ||||
|             }, | ||||
|           }, | ||||
|           2, | ||||
|           3, | ||||
|         ); | ||||
|       }); | ||||
|     }); | ||||
|   | ||||
| @@ -11,7 +11,6 @@ import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../website/server/models/group'; | ||||
| import { | ||||
|   generateGroup, | ||||
|   sleep, | ||||
| } from '../../../../../helpers/api-unit.helper'; | ||||
|  | ||||
| describe('Purchasing a group plan for group', () => { | ||||
| @@ -293,7 +292,7 @@ describe('Purchasing a group plan for group', () => { | ||||
|   }); | ||||
|  | ||||
|   it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => { | ||||
|     const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL'); | ||||
|     const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL'); | ||||
|     plan.customerId = 'random'; | ||||
|     plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD; | ||||
|  | ||||
| @@ -308,26 +307,46 @@ describe('Purchasing a group plan for group', () => { | ||||
|     data.groupId = group._id; | ||||
|  | ||||
|     await api.createSubscription(data); | ||||
|     await sleep(0.5); | ||||
|  | ||||
|     expect(sender.sendTxn).to.have.callCount(4); | ||||
|     expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL); | ||||
|     expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details'); | ||||
|     expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id); | ||||
|     expect(sender.sendTxn.args[1][1]).to.equal('group-member-join'); | ||||
|     expect(sender.sendTxn.args[1][2]).to.eql([ | ||||
|     const adminUserSubscriptionDetails = sender.sendTxn.args.find(sendTxnArgs => { | ||||
|       const emailType = sendTxnArgs[1]; | ||||
|       return emailType === 'admin-user-subscription-details'; | ||||
|     }); | ||||
|     expect(adminUserSubscriptionDetails).to.exist; | ||||
|     expect(adminUserSubscriptionDetails[0].email).to.equal(TECH_ASSISTANCE_EMAIL); | ||||
|  | ||||
|     const groupMemberJoinOne = sender.sendTxn.args.find(sendTxnArgs => { | ||||
|       const emailType = sendTxnArgs[1]; | ||||
|       const emailRecipient = sendTxnArgs[0]; | ||||
|       return emailType === 'group-member-join' && emailRecipient._id === recipient._id; | ||||
|     }); | ||||
|     expect(groupMemberJoinOne).to.exist; | ||||
|     expect(groupMemberJoinOne[0]._id).to.equal(recipient._id); | ||||
|     expect(groupMemberJoinOne[2]).to.eql([ | ||||
|       { name: 'LEADER', content: groupLeaderName }, | ||||
|       { name: 'GROUP_NAME', content: groupName }, | ||||
|       { name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE }, | ||||
|     ]); | ||||
|     expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader); | ||||
|     expect(sender.sendTxn.args[2][1]).to.equal('group-member-join'); | ||||
|     expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader); | ||||
|     expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins'); | ||||
|  | ||||
|     const groupMemberJoinTwo = sender.sendTxn.args.find(sendTxnArgs => { | ||||
|       const emailType = sendTxnArgs[1]; | ||||
|       const emailRecipient = sendTxnArgs[0]; | ||||
|       return emailType === 'group-member-join' && emailRecipient._id === group.leader; | ||||
|     }); | ||||
|     expect(groupMemberJoinTwo).to.exist; | ||||
|     expect(groupMemberJoinTwo[0]._id).to.equal(group.leader); | ||||
|  | ||||
|     const groupSubscriptionBegins = sender.sendTxn.args.find(sendTxnArgs => { | ||||
|       const emailType = sendTxnArgs[1]; | ||||
|       return emailType === 'group-subscription-begins'; | ||||
|     }); | ||||
|     expect(groupSubscriptionBegins).to.exist; | ||||
|     expect(groupSubscriptionBegins[0]._id).to.equal(group.leader); | ||||
|   }); | ||||
|  | ||||
|   it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => { | ||||
|     const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL'); | ||||
|     const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL'); | ||||
|     plan.customerId = 'random'; | ||||
|     plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD; | ||||
|  | ||||
| @@ -342,22 +361,43 @@ describe('Purchasing a group plan for group', () => { | ||||
|     data.groupId = group._id; | ||||
|  | ||||
|     await api.createSubscription(data); | ||||
|     await sleep(0.5); | ||||
|  | ||||
|     expect(sender.sendTxn).to.have.callCount(4); | ||||
|     expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL); | ||||
|     expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details'); | ||||
|     expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id); | ||||
|     expect(sender.sendTxn.args[1][1]).to.equal('group-member-join'); | ||||
|     expect(sender.sendTxn.args[1][2]).to.eql([ | ||||
|  | ||||
|     const adminUserSubscriptionDetails = sender.sendTxn.args.find(sendTxnArgs => { | ||||
|       const emailType = sendTxnArgs[1]; | ||||
|       return emailType === 'admin-user-subscription-details'; | ||||
|     }); | ||||
|     expect(adminUserSubscriptionDetails).to.exist; | ||||
|     expect(adminUserSubscriptionDetails[0].email).to.equal(TECH_ASSISTANCE_EMAIL); | ||||
|  | ||||
|     const groupMemberJoinOne = sender.sendTxn.args.find(sendTxnArgs => { | ||||
|       const emailType = sendTxnArgs[1]; | ||||
|       const emailRecipient = sendTxnArgs[0]; | ||||
|       return emailType === 'group-member-join' && emailRecipient._id === recipient._id; | ||||
|     }); | ||||
|     expect(groupMemberJoinOne).to.exist; | ||||
|     expect(groupMemberJoinOne[0]._id).to.equal(recipient._id); | ||||
|     expect(groupMemberJoinOne[2]).to.eql([ | ||||
|       { name: 'LEADER', content: groupLeaderName }, | ||||
|       { name: 'GROUP_NAME', content: groupName }, | ||||
|       { name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS }, | ||||
|     ]); | ||||
|     expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader); | ||||
|     expect(sender.sendTxn.args[2][1]).to.equal('group-member-join'); | ||||
|     expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader); | ||||
|     expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins'); | ||||
|  | ||||
|     const groupMemberJoinTwo = sender.sendTxn.args.find(sendTxnArgs => { | ||||
|       const emailType = sendTxnArgs[1]; | ||||
|       const emailRecipient = sendTxnArgs[0]; | ||||
|       return emailType === 'group-member-join' && emailRecipient._id === group.leader; | ||||
|     }); | ||||
|     expect(groupMemberJoinTwo).to.exist; | ||||
|     expect(groupMemberJoinTwo[0]._id).to.equal(group.leader); | ||||
|  | ||||
|     const groupSubscriptionBegins = sender.sendTxn.args.find(sendTxnArgs => { | ||||
|       const emailType = sendTxnArgs[1]; | ||||
|       return emailType === 'group-subscription-begins'; | ||||
|     }); | ||||
|     expect(groupSubscriptionBegins).to.exist; | ||||
|     expect(groupSubscriptionBegins[0]._id).to.equal(group.leader); | ||||
|   }); | ||||
|  | ||||
|   it('adds months to members with existing gift subscription', async () => { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import got from 'got'; | ||||
| import moment from 'moment'; | ||||
| import { | ||||
|   WebhookSender, | ||||
|   taskScoredWebhook, | ||||
| @@ -13,6 +14,7 @@ import { | ||||
| import { | ||||
|   generateUser, | ||||
|   defer, | ||||
|   sleep, | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
|  | ||||
|  | ||||
| @@ -99,8 +101,7 @@ describe('webhooks', () => { | ||||
|       expect(WebhookSender.defaultTransformData).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|         json: true, | ||||
|         body, | ||||
|         json: body, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
| @@ -120,7 +121,7 @@ describe('webhooks', () => { | ||||
|       expect(sendWebhook.attachDefaultData).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|         json: true, | ||||
|         json: body, | ||||
|       }); | ||||
|  | ||||
|       expect(body).to.eql({ | ||||
| @@ -151,8 +152,7 @@ describe('webhooks', () => { | ||||
|       expect(WebhookSender.defaultTransformData).to.not.be.called; | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|         json: true, | ||||
|         body: { | ||||
|         json: { | ||||
|           foo: 'bar', | ||||
|           baz: 'biz', | ||||
|         }, | ||||
| @@ -269,8 +269,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|         body, | ||||
|         json: true, | ||||
|         json: body, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
| @@ -290,8 +289,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|         body, | ||||
|         json: true, | ||||
|         json: body, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
| @@ -314,12 +312,101 @@ describe('webhooks', () => { | ||||
|  | ||||
|       expect(got.post).to.be.calledTwice; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|         body, | ||||
|         json: true, | ||||
|         json: body, | ||||
|       }); | ||||
|       expect(got.post).to.be.calledWithMatch('http://other-url.com', { | ||||
|         body, | ||||
|         json: true, | ||||
|         json: body, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     describe('failures', () => { | ||||
|       let sendWebhook; | ||||
|  | ||||
|       beforeEach(async () => { | ||||
|         sandbox.restore(); | ||||
|         sandbox.stub(got, 'post').returns(Promise.reject()); | ||||
|  | ||||
|         sendWebhook = new WebhookSender({ type: 'taskActivity' }); | ||||
|         user.webhooks = [{ | ||||
|           url: 'http://custom-url.com', enabled: true, type: 'taskActivity', | ||||
|         }]; | ||||
|         await user.save(); | ||||
|  | ||||
|         expect(user.webhooks[0].failures).to.equal(0); | ||||
|         expect(user.webhooks[0].lastFailureAt).to.equal(undefined); | ||||
|       }); | ||||
|  | ||||
|       it('does not increase failures counter if request is successfull', async () => { | ||||
|         sandbox.restore(); | ||||
|         sandbox.stub(got, 'post').returns(Promise.resolve()); | ||||
|  | ||||
|         const body = {}; | ||||
|         sendWebhook.send(user, body); | ||||
|  | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|           json: body, | ||||
|         }); | ||||
|  | ||||
|         await sleep(0.1); | ||||
|         user = await User.findById(user._id).exec(); | ||||
|  | ||||
|         expect(user.webhooks[0].failures).to.equal(0); | ||||
|         expect(user.webhooks[0].lastFailureAt).to.equal(undefined); | ||||
|       }); | ||||
|  | ||||
|       it('records failures', async () => { | ||||
|         const body = {}; | ||||
|         sendWebhook.send(user, body); | ||||
|  | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|           json: body, | ||||
|         }); | ||||
|  | ||||
|         await sleep(0.1); | ||||
|         user = await User.findById(user._id).exec(); | ||||
|  | ||||
|         expect(user.webhooks[0].failures).to.equal(1); | ||||
|         expect((Date.now() - user.webhooks[0].lastFailureAt.getTime()) < 10000).to.be.true; | ||||
|       }); | ||||
|  | ||||
|       it('disables a webhook after 10 failures', async () => { | ||||
|         const times = 10; | ||||
|         for (let i = 0; i < times; i += 1) { | ||||
|           sendWebhook.send(user, {}); | ||||
|           await sleep(0.1); // eslint-disable-line no-await-in-loop | ||||
|           user = await User.findById(user._id).exec(); // eslint-disable-line no-await-in-loop | ||||
|         } | ||||
|  | ||||
|         expect(got.post).to.be.callCount(10); | ||||
|         expect(got.post).to.be.calledWithMatch('http://custom-url.com'); | ||||
|  | ||||
|         await sleep(0.1); | ||||
|         user = await User.findById(user._id).exec(); | ||||
|  | ||||
|         expect(user.webhooks[0].enabled).to.equal(false); | ||||
|         expect(user.webhooks[0].failures).to.equal(0); | ||||
|       }); | ||||
|  | ||||
|       it('resets failures after a month ', async () => { | ||||
|         const oneMonthAgo = moment().subtract(1, 'months').subtract(1, 'days').toDate(); | ||||
|         user.webhooks[0].lastFailureAt = oneMonthAgo; | ||||
|         user.webhooks[0].failures = 9; | ||||
|  | ||||
|         await user.save(); | ||||
|  | ||||
|         sendWebhook.send(user, []); | ||||
|  | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch('http://custom-url.com'); | ||||
|  | ||||
|         await sleep(0.1); | ||||
|         user = await User.findById(user._id).exec(); | ||||
|  | ||||
|         expect(user.webhooks[0].failures).to.equal(1); | ||||
|         // Check that the stored date is whitin 10s from now | ||||
|         expect((Date.now() - user.webhooks[0].lastFailureAt.getTime()) < 10000).to.be.true; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| @@ -364,8 +451,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch(webhooks[0].url, { | ||||
|         json: true, | ||||
|         body: { | ||||
|         json: { | ||||
|           type: 'scored', | ||||
|           webhookType: 'taskActivity', | ||||
|           user: { | ||||
| @@ -402,8 +488,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://global-activity.com', { | ||||
|         json: true, | ||||
|         body: { | ||||
|         json: { | ||||
|           type: 'scored', | ||||
|           webhookType: 'taskActivity', | ||||
|           user: { | ||||
| @@ -456,8 +541,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch(webhooks[0].url, { | ||||
|           json: true, | ||||
|           body: { | ||||
|           json: { | ||||
|             type, | ||||
|             webhookType: 'taskActivity', | ||||
|             user: { | ||||
| @@ -497,8 +581,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch(webhooks[0].url, { | ||||
|           json: true, | ||||
|           body: { | ||||
|           json: { | ||||
|             webhookType: 'taskActivity', | ||||
|             user: { | ||||
|               _id: user._id, | ||||
| @@ -538,8 +621,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch(webhooks[2].url, { | ||||
|           json: true, | ||||
|           body: { | ||||
|           json: { | ||||
|             type, | ||||
|             webhookType: 'userActivity', | ||||
|             user: { | ||||
| @@ -585,8 +667,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch(webhooks[1].url, { | ||||
|           json: true, | ||||
|           body: { | ||||
|           json: { | ||||
|             type, | ||||
|             webhookType: 'questActivity', | ||||
|             user: { | ||||
| @@ -632,8 +713,7 @@ describe('webhooks', () => { | ||||
|  | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, { | ||||
|         json: true, | ||||
|         body: { | ||||
|         json: { | ||||
|           webhookType: 'groupChatReceived', | ||||
|           user: { | ||||
|             _id: user._id, | ||||
|   | ||||
| @@ -19,7 +19,7 @@ describe('analytics middleware', () => { | ||||
|     next = generateNext(); | ||||
|   }); | ||||
|  | ||||
|   it('attaches analytics object res.locals', () => { | ||||
|   it('attaches analytics object to res', () => { | ||||
|     const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default; | ||||
|  | ||||
|     attachAnalytics(req, res, next); | ||||
|   | ||||
| @@ -21,28 +21,11 @@ describe('cron middleware', () => { | ||||
|     req; | ||||
|   let user; | ||||
|  | ||||
|   beforeEach(done => { | ||||
|   beforeEach(async () => { | ||||
|     res = generateRes(); | ||||
|     req = generateReq(); | ||||
|     user = new User({ | ||||
|       auth: { | ||||
|         local: { | ||||
|           username: 'username', | ||||
|           lowerCaseUsername: 'username', | ||||
|           email: 'email@email.email', | ||||
|           salt: 'salt', | ||||
|           hashed_password: 'hashed_password', // eslint-disable-line camelcase | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     user.save() | ||||
|       .then(savedUser => { | ||||
|         res.locals.user = savedUser; | ||||
|         res.analytics = analyticsService; | ||||
|         done(); | ||||
|       }) | ||||
|       .catch(done); | ||||
|     user = await res.locals.user.save(); | ||||
|     res.analytics = analyticsService; | ||||
|   }); | ||||
|  | ||||
|   afterEach(() => { | ||||
|   | ||||
							
								
								
									
										94
									
								
								test/api/unit/middlewares/ipBlocker.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,94 @@ | ||||
| import nconf from 'nconf'; | ||||
| import requireAgain from 'require-again'; | ||||
| import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import { Forbidden } from '../../../../website/server/libs/errors'; | ||||
| import apiError from '../../../../website/server/libs/apiError'; | ||||
|  | ||||
| function checkErrorThrown (next) { | ||||
|   expect(next).to.have.been.calledOnce; | ||||
|   const calledWith = next.getCall(0).args; | ||||
|   expect(calledWith[0].message).to.equal(apiError('ipAddressBlocked')); | ||||
|   expect(calledWith[0] instanceof Forbidden).to.equal(true); | ||||
| } | ||||
|  | ||||
| function checkErrorNotThrown (next) { | ||||
|   expect(next).to.have.been.calledOnce; | ||||
|   const calledWith = next.getCall(0).args; | ||||
|   expect(typeof calledWith[0] === 'undefined').to.equal(true); | ||||
| } | ||||
|  | ||||
| describe('ipBlocker middleware', () => { | ||||
|   const pathToIpBlocker = '../../../../website/server/middlewares/ipBlocker'; | ||||
|  | ||||
|   let res; let req; let next; | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     res = generateRes(); | ||||
|     req = generateReq(); | ||||
|     next = generateNext(); | ||||
|   }); | ||||
|  | ||||
|   it('is disabled when the env var is not defined', () => { | ||||
|     sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(undefined); | ||||
|     const attachIpBlocker = requireAgain(pathToIpBlocker).default; | ||||
|     attachIpBlocker(req, res, next); | ||||
|  | ||||
|     checkErrorNotThrown(next); | ||||
|   }); | ||||
|  | ||||
|   it('is disabled when the env var is an empty string', () => { | ||||
|     sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(''); | ||||
|     const attachIpBlocker = requireAgain(pathToIpBlocker).default; | ||||
|     attachIpBlocker(req, res, next); | ||||
|  | ||||
|     checkErrorNotThrown(next); | ||||
|   }); | ||||
|  | ||||
|   it('is disabled when the env var contains comma separated empty strings', () => { | ||||
|     sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(' , , '); | ||||
|     const attachIpBlocker = requireAgain(pathToIpBlocker).default; | ||||
|     attachIpBlocker(req, res, next); | ||||
|  | ||||
|     checkErrorNotThrown(next); | ||||
|   }); | ||||
|  | ||||
|   it('does not throw when the ip does not match', () => { | ||||
|     req.headers['x-forwarded-for'] = '192.168.1.1'; | ||||
|     sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.2'); | ||||
|     const attachIpBlocker = requireAgain(pathToIpBlocker).default; | ||||
|     attachIpBlocker(req, res, next); | ||||
|  | ||||
|     checkErrorNotThrown(next); | ||||
|   }); | ||||
|  | ||||
|   it('throws when a matching ip exist in x-forwarded-for', () => { | ||||
|     req.headers['x-forwarded-for'] = '192.168.1.1'; | ||||
|     sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1'); | ||||
|     const attachIpBlocker = requireAgain(pathToIpBlocker).default; | ||||
|     attachIpBlocker(req, res, next); | ||||
|  | ||||
|     checkErrorThrown(next); | ||||
|   }); | ||||
|  | ||||
|   it('trims ips in x-forwarded-for', () => { | ||||
|     req.headers['x-forwarded-for'] = '192.168.1.1'; | ||||
|     sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(',  192.168.1.1 ,   192.168.1.4, '); | ||||
|     const attachIpBlocker = requireAgain(pathToIpBlocker).default; | ||||
|     attachIpBlocker(req, res, next); | ||||
|  | ||||
|     checkErrorThrown(next); | ||||
|   }); | ||||
|  | ||||
|   it('works when multiple ips are passed in x-forwarded-for', () => { | ||||
|     req.headers['x-forwarded-for'] = '192.168.1.4'; | ||||
|     sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1, 192.168.1.4, 192.168.1.3'); | ||||
|     const attachIpBlocker = requireAgain(pathToIpBlocker).default; | ||||
|     attachIpBlocker(req, res, next); | ||||
|  | ||||
|     checkErrorThrown(next); | ||||
|   }); | ||||
| }); | ||||
| @@ -12,6 +12,9 @@ import { model as User } from '../../../../website/server/models/user'; | ||||
|  | ||||
| const { i18n } = common; | ||||
|  | ||||
| // TODO some of the checks here can be simplified to simply check | ||||
| // that the right parameters are passed to the functions in libs/language | ||||
|  | ||||
| describe('language middleware', () => { | ||||
|   describe('res.t', () => { | ||||
|     let res; let req; let | ||||
| @@ -19,6 +22,8 @@ describe('language middleware', () => { | ||||
|  | ||||
|     beforeEach(() => { | ||||
|       res = generateRes(); | ||||
|       // remove the defaul user | ||||
|       res.locals.user = undefined; | ||||
|       req = generateReq(); | ||||
|       next = generateNext(); | ||||
|  | ||||
| @@ -57,6 +62,8 @@ describe('language middleware', () => { | ||||
|  | ||||
|     beforeEach(() => { | ||||
|       res = generateRes(); | ||||
|       // remove the defaul user | ||||
|       res.locals.user = undefined; | ||||
|       req = generateReq(); | ||||
|       next = generateNext(); | ||||
|       attachTranslateFunction(req, res, next); | ||||
| @@ -88,7 +95,7 @@ describe('language middleware', () => { | ||||
|           lang: 'es', | ||||
|         }; | ||||
|  | ||||
|         req.locals = { | ||||
|         res.locals = { | ||||
|           user: { | ||||
|             preferences: { | ||||
|               language: 'it', | ||||
| @@ -108,7 +115,7 @@ describe('language middleware', () => { | ||||
|  | ||||
|     context('authorized request', () => { | ||||
|       it('uses the user preferred language if avalaible', () => { | ||||
|         req.locals = { | ||||
|         res.locals = { | ||||
|           user: { | ||||
|             preferences: { | ||||
|               language: 'it', | ||||
| @@ -122,7 +129,7 @@ describe('language middleware', () => { | ||||
|       }); | ||||
|  | ||||
|       it('falls back to english if the user preferred language is not avalaible', done => { | ||||
|         req.locals = { | ||||
|         res.locals = { | ||||
|           user: { | ||||
|             preferences: { | ||||
|               language: 'bla', | ||||
| @@ -138,7 +145,7 @@ describe('language middleware', () => { | ||||
|       }); | ||||
|  | ||||
|       it('uses the user preferred language even if a session is included in request', () => { | ||||
|         req.locals = { | ||||
|         res.locals = { | ||||
|           user: { | ||||
|             preferences: { | ||||
|               language: 'it', | ||||
|   | ||||
| @@ -187,8 +187,7 @@ describe('GET challenges/groups/:groupId', () => { | ||||
|   }); | ||||
|  | ||||
|   context('official challenge is present', () => { | ||||
|     let publicGuild; let user; let officialChallenge; let challenge; let | ||||
|       challenge2; | ||||
|     let publicGuild; let user; let officialChallenge; let unofficialChallenges; | ||||
|  | ||||
|     before(async () => { | ||||
|       const { group, groupLeader } = await createAndPopulateGroup({ | ||||
| @@ -214,10 +213,14 @@ describe('GET challenges/groups/:groupId', () => { | ||||
|       }); | ||||
|       await user.post(`/challenges/${officialChallenge._id}/join`); | ||||
|  | ||||
|       challenge = await generateChallenge(user, group); | ||||
|       await user.post(`/challenges/${challenge._id}/join`); | ||||
|       challenge2 = await generateChallenge(user, group); | ||||
|       await user.post(`/challenges/${challenge2._id}/join`); | ||||
|       // We add 10 extra challenges to test whether the official challenge | ||||
|       // (the oldest) makes it to the front page. | ||||
|       unofficialChallenges = []; | ||||
|       for (let i = 0; i < 10; i += 1) { | ||||
|         const challenge = await generateChallenge(user, group); // eslint-disable-line | ||||
|         await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line | ||||
|         unofficialChallenges.push(challenge); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     it('should return official challenges first', async () => { | ||||
| @@ -230,18 +233,17 @@ describe('GET challenges/groups/:groupId', () => { | ||||
|     it('should return newest challenges first, after official ones', async () => { | ||||
|       let challenges = await user.get(`/challenges/groups/${publicGuild._id}`); | ||||
|  | ||||
|       let foundChallengeIndex = _.findIndex(challenges, { _id: challenge._id }); | ||||
|       expect(foundChallengeIndex).to.eql(2); | ||||
|  | ||||
|       foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id }); | ||||
|       expect(foundChallengeIndex).to.eql(1); | ||||
|       unofficialChallenges.forEach((chal, index) => { | ||||
|         const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id }); | ||||
|         expect(foundChallengeIndex).to.eql(10 - index); | ||||
|       }); | ||||
|  | ||||
|       const newChallenge = await generateChallenge(user, publicGuild); | ||||
|       await user.post(`/challenges/${newChallenge._id}/join`); | ||||
|  | ||||
|       challenges = await user.get(`/challenges/groups/${publicGuild._id}`); | ||||
|  | ||||
|       foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id }); | ||||
|       const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id }); | ||||
|       expect(foundChallengeIndex).to.eql(1); | ||||
|     }); | ||||
|   }); | ||||
|   | ||||
| @@ -242,7 +242,7 @@ describe('GET challenges/user', () => { | ||||
|   }); | ||||
|  | ||||
|   context('official challenge is present', () => { | ||||
|     let user; let officialChallenge; let challenge; let challenge2; let | ||||
|     let user; let officialChallenge; let unofficialChallenges; let | ||||
|       publicGuild; | ||||
|  | ||||
|     before(async () => { | ||||
| @@ -270,10 +270,14 @@ describe('GET challenges/user', () => { | ||||
|       }); | ||||
|       await user.post(`/challenges/${officialChallenge._id}/join`); | ||||
|  | ||||
|       challenge = await generateChallenge(user, group); | ||||
|       await user.post(`/challenges/${challenge._id}/join`); | ||||
|       challenge2 = await generateChallenge(user, group); | ||||
|       await user.post(`/challenges/${challenge2._id}/join`); | ||||
|       // We add 10 extra challenges to test whether the official challenge | ||||
|       // (the oldest) makes it to the front page. | ||||
|       unofficialChallenges = []; | ||||
|       for (let i = 0; i < 10; i += 1) { | ||||
|         const challenge = await generateChallenge(user, group); // eslint-disable-line | ||||
|         await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line | ||||
|         unofficialChallenges.push(challenge); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     it('should return official challenges first', async () => { | ||||
| @@ -284,20 +288,23 @@ describe('GET challenges/user', () => { | ||||
|     }); | ||||
|  | ||||
|     it('should return newest challenges first, after official ones', async () => { | ||||
|       let challenges = await user.get('/challenges/user'); | ||||
|       let challenges = await user.get('/challenges/user?page=0'); | ||||
|  | ||||
|       let foundChallengeIndex = _.findIndex(challenges, { _id: challenge._id }); | ||||
|       expect(foundChallengeIndex).to.eql(2); | ||||
|  | ||||
|       foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id }); | ||||
|       expect(foundChallengeIndex).to.eql(1); | ||||
|       unofficialChallenges.forEach((chal, index) => { | ||||
|         const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id }); | ||||
|         if (index === 0) { | ||||
|           expect(foundChallengeIndex).to.eql(-1); | ||||
|         } else { | ||||
|           expect(foundChallengeIndex).to.eql(10 - index); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       const newChallenge = await generateChallenge(user, publicGuild); | ||||
|       await user.post(`/challenges/${newChallenge._id}/join`); | ||||
|  | ||||
|       challenges = await user.get('/challenges/user'); | ||||
|  | ||||
|       foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id }); | ||||
|       const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id }); | ||||
|       expect(foundChallengeIndex).to.eql(1); | ||||
|     }); | ||||
|   }); | ||||
|   | ||||
| @@ -11,7 +11,9 @@ import apiError from '../../../../../website/server/libs/apiError'; | ||||
|  | ||||
| describe('GET /groups', () => { | ||||
|   let user; | ||||
|   let userInGuild; | ||||
|   const NUMBER_OF_PUBLIC_GUILDS = 3; // 2 + the tavern | ||||
|   const NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER = 2; | ||||
|   const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1; | ||||
|   const NUMBER_OF_USERS_PRIVATE_GUILDS = 1; | ||||
|   const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5; | ||||
| @@ -33,14 +35,20 @@ describe('GET /groups', () => { | ||||
|       name: 'public guild - is member', | ||||
|       type: 'guild', | ||||
|       privacy: 'public', | ||||
|       summary: 'ohayou kombonwa', | ||||
|       description: 'oyasumi', | ||||
|     }); | ||||
|     await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] }); | ||||
|     await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`); | ||||
|  | ||||
|     userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] }); | ||||
|  | ||||
|     publicGuildNotMember = await generateGroup(leader, { | ||||
|       name: 'public guild - is not member', | ||||
|       type: 'guild', | ||||
|       privacy: 'public', | ||||
|       summary: 'Natsume Soseki', | ||||
|       description: 'Kinnosuke no Hondana', | ||||
|       categories, | ||||
|     }); | ||||
|  | ||||
| @@ -150,6 +158,35 @@ describe('GET /groups', () => { | ||||
|  | ||||
|       expect(guilds.length).to.equal(0); | ||||
|     }); | ||||
|  | ||||
|     it('filters public guilds by leader role', async () => { | ||||
|       const guilds = await user.get('/groups?type=publicGuilds&leader=true'); | ||||
|       expect(guilds.length).to.equal(NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER); | ||||
|     }); | ||||
|  | ||||
|     it('filters public guilds by member role', async () => { | ||||
|       const guilds = await userInGuild.get('/groups?type=publicGuilds&member=true'); | ||||
|       expect(guilds.length).to.equal(1); | ||||
|       expect(guilds[0].name).to.have.string('is member'); | ||||
|     }); | ||||
|  | ||||
|     it('filters public guilds by single-word search term', async () => { | ||||
|       const guilds = await user.get('/groups?type=publicGuilds&search=kom'); | ||||
|       expect(guilds.length).to.equal(1); | ||||
|       expect(guilds[0].summary).to.have.string('ohayou kombonwa'); | ||||
|     }); | ||||
|  | ||||
|     it('filters public guilds by single-word search term left and right-padded by spaces', async () => { | ||||
|       const guilds = await user.get('/groups?type=publicGuilds&search=++++ohayou+kombonwa+++++'); | ||||
|       expect(guilds.length).to.equal(1); | ||||
|       expect(guilds[0].summary).to.have.string('ohayou kombonwa'); | ||||
|     }); | ||||
|  | ||||
|     it('filters public guilds by two-words search term separated by multiple spaces', async () => { | ||||
|       const guilds = await user.get('/groups?type=publicGuilds&search=kinnosuke+++++hon'); | ||||
|       expect(guilds.length).to.equal(1); | ||||
|       expect(guilds[0].description).to.have.string('Kinnosuke'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('public guilds pagination', () => { | ||||
|   | ||||
| @@ -17,7 +17,7 @@ describe('payments - stripe - #checkout', () => { | ||||
|     await expect(user.post(endpoint, { id: 123 })).to.eventually.be.rejected.and.include({ | ||||
|       code: 401, | ||||
|       error: 'Error', | ||||
|       message: 'Invalid API Key provided: aaaabbbb********************1111', | ||||
|       // message: 'Invalid API Key provided: aaaabbbb********************1111', | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   | ||||
| @@ -55,6 +55,18 @@ describe('POST /tasks/user', () => { | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('returns an error if reward value is a negative number', async () => { | ||||
|       await expect(user.post('/tasks/user', { | ||||
|         type: 'reward', | ||||
|         text: 'reward with negative value', | ||||
|         value: -10, | ||||
|       })).to.eventually.be.rejected.and.eql({ | ||||
|         code: 400, | ||||
|         error: 'BadRequest', | ||||
|         message: 'reward validation failed', | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('does not update user.tasksOrder.{taskType} when the task is not saved because invalid', async () => { | ||||
|       const originalHabitsOrder = (await user.get('/user')).tasksOrder.habits; | ||||
|       await expect(user.post('/tasks/user', { | ||||
|   | ||||
| @@ -530,5 +530,15 @@ describe('PUT /tasks/:id', () => { | ||||
|  | ||||
|       expect(savedReward.value).to.eql(100); | ||||
|     }); | ||||
|  | ||||
|     it('returns an error if reward value is a negative number', async () => { | ||||
|       await expect(user.put(`/tasks/${reward._id}`, { | ||||
|         value: -10, | ||||
|       })).to.eventually.be.rejected.and.eql({ | ||||
|         code: 400, | ||||
|         error: 'BadRequest', | ||||
|         message: 'reward validation failed', | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -81,6 +81,16 @@ describe('POST /user/webhook', () => { | ||||
|     expect(webhook.type).to.eql('taskActivity'); | ||||
|   }); | ||||
|  | ||||
|   it('ignores protected fields', async () => { | ||||
|     body.failures = 3; | ||||
|     body.lastFailureAt = new Date(); | ||||
|  | ||||
|     const webhook = await user.post('/user/webhook', body); | ||||
|  | ||||
|     expect(webhook.failures).to.eql(0); | ||||
|     expect(webhook.lastFailureAt).to.eql(undefined); | ||||
|   }); | ||||
|  | ||||
|   it('successfully adds the webhook', async () => { | ||||
|     expect(user.webhooks).to.eql([]); | ||||
|  | ||||
|   | ||||
| @@ -63,6 +63,21 @@ describe('PUT /user/webhook/:id', () => { | ||||
|     expect(webhook.options).to.eql(options); | ||||
|   }); | ||||
|  | ||||
|   it('ignores protected fields', async () => { | ||||
|     const failures = 3; | ||||
|     const lastFailureAt = new Date(); | ||||
|  | ||||
|     await user.put(`/user/webhook/${webhookToUpdate.id}`, { | ||||
|       failures, lastFailureAt, | ||||
|     }); | ||||
|  | ||||
|     await user.sync(); | ||||
|     const webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id); | ||||
|  | ||||
|     expect(webhook.failures).to.eql(0); | ||||
|     expect(webhook.lastFailureAt).to.eql(undefined); | ||||
|   }); | ||||
|  | ||||
|   it('updates a webhook with empty label', async () => { | ||||
|     const url = 'http://a-new-url.com'; | ||||
|     const type = 'groupChatReceived'; | ||||
|   | ||||
							
								
								
									
										1987
									
								
								website/client/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -18,24 +18,24 @@ | ||||
|     "@vue/cli-plugin-router": "^4.2.3", | ||||
|     "@vue/cli-plugin-unit-mocha": "^4.2.3", | ||||
|     "@vue/cli-service": "^4.2.3", | ||||
|     "@storybook/addon-actions": "^5.3.14", | ||||
|     "@storybook/addon-knobs": "^5.3.14", | ||||
|     "@storybook/addon-links": "^5.3.14", | ||||
|     "@storybook/addon-notes": "^5.3.14", | ||||
|     "@storybook/vue": "^5.3.14", | ||||
|     "@storybook/addon-actions": "^5.3.17", | ||||
|     "@storybook/addon-knobs": "^5.3.17", | ||||
|     "@storybook/addon-links": "^5.3.17", | ||||
|     "@storybook/addon-notes": "^5.3.17", | ||||
|     "@storybook/vue": "^5.3.17", | ||||
|     "@vue/test-utils": "1.0.0-beta.29", | ||||
|     "amplitude-js": "^5.9.0", | ||||
|     "amplitude-js": "^5.10.0", | ||||
|     "axios": "^0.19.2", | ||||
|     "axios-progress-bar": "^1.2.0", | ||||
|     "babel-eslint": "^10.1.0", | ||||
|     "bootstrap": "^4.4.1", | ||||
|     "bootstrap-vue": "^2.5.0", | ||||
|     "bootstrap-vue": "^2.9.0", | ||||
|     "chai": "^4.1.2", | ||||
|     "core-js": "^3.6.4", | ||||
|     "eslint": "^6.8.0", | ||||
|     "eslint-config-habitrpg": "^6.2.0", | ||||
|     "eslint-plugin-mocha": "^5.3.0", | ||||
|     "eslint-plugin-vue": "^6.2.1", | ||||
|     "eslint-plugin-vue": "^6.2.2", | ||||
|     "habitica-markdown": "^1.3.2", | ||||
|     "hellojs": "^1.18.4", | ||||
|     "inspectpack": "^4.4.0", | ||||
| @@ -44,7 +44,7 @@ | ||||
|     "lodash": "^4.17.15", | ||||
|     "moment": "^2.24.0", | ||||
|     "nconf": "^0.10.0", | ||||
|     "sass": "^1.26.2", | ||||
|     "sass": "^1.26.3", | ||||
|     "sass-loader": "^8.0.2", | ||||
|     "smartbanner.js": "^1.15.0", | ||||
|     "svg-inline-loader": "^0.8.2", | ||||
| @@ -58,9 +58,9 @@ | ||||
|     "vue-mugen-scroll": "^0.2.6", | ||||
|     "vue-router": "^3.1.6", | ||||
|     "vue-template-compiler": "^2.6.11", | ||||
|     "vue2-perfect-scrollbar": "^1.3.0", | ||||
|     "vue2-perfect-scrollbar": "^1.4.0", | ||||
|     "vuedraggable": "^2.23.1", | ||||
|     "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", | ||||
|     "webpack": "^4.42.0" | ||||
|     "webpack": "^4.42.1" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -33,7 +33,7 @@ | ||||
|         'resting': showRestingBanner | ||||
|       }" | ||||
|     > | ||||
|       <banned-account-modal /> | ||||
|       <!-- <banned-account-modal /> --> | ||||
|       <amazon-payments-modal v-if="!isStaticPage" /> | ||||
|       <payments-success-modal /> | ||||
|       <sub-cancel-modal-confirm v-if="isUserLoaded" /> | ||||
| @@ -266,7 +266,6 @@ import { | ||||
| } from '@/libs/userlocalManager'; | ||||
|  | ||||
| import svgClose from '@/assets/svg/close.svg'; | ||||
| import bannedAccountModal from '@/components/bannedAccountModal'; | ||||
|  | ||||
| const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line | ||||
|  | ||||
| @@ -281,7 +280,6 @@ export default { | ||||
|     BuyModal, | ||||
|     SelectMembersModal, | ||||
|     amazonPaymentsModal, | ||||
|     bannedAccountModal, | ||||
|     paymentsSuccessModal, | ||||
|     subCancelModalConfirm, | ||||
|     subCanceledModal, | ||||
| @@ -385,7 +383,8 @@ export default { | ||||
|       return response; | ||||
|     }, error => { | ||||
|       if (error.response.status >= 400) { | ||||
|         this.checkForBannedUser(error); | ||||
|         const isBanned = this.checkForBannedUser(error); | ||||
|         if (isBanned === true) return null; // eslint-disable-line consistent-return | ||||
|  | ||||
|         // Don't show errors from getting user details. These users have delete their account, | ||||
|         // but their chat message still exists. | ||||
| @@ -403,7 +402,8 @@ export default { | ||||
|         // TODO use a specific error like NotificationNotFound instead of checking for the string | ||||
|         const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.']; | ||||
|         if (invalidUserMessage.indexOf(errorMessage) !== -1) { | ||||
|           this.$store.dispatch('auth:logout'); | ||||
|           this.$store.dispatch('auth:logout', { redirectToLogin: true }); | ||||
|           return null; | ||||
|         } | ||||
|  | ||||
|         // Most server errors should return is click to dismiss errors, with some exceptions | ||||
| @@ -553,7 +553,7 @@ export default { | ||||
|  | ||||
|       // Case where user is not logged in | ||||
|       if (!parseSettings) { | ||||
|         return; | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       const bannedMessage = this.$t('accountSuspended', { | ||||
| @@ -561,9 +561,10 @@ export default { | ||||
|         userId: parseSettings.auth.apiId, | ||||
|       }); | ||||
|  | ||||
|       if (errorMessage !== bannedMessage) return; | ||||
|       if (errorMessage !== bannedMessage) return false; | ||||
|  | ||||
|       this.$root.$emit('bv::show::modal', 'banned-account'); | ||||
|       this.$store.dispatch('auth:logout', { redirectToLogin: true }); | ||||
|       return true; | ||||
|     }, | ||||
|     initializeModalStack () { | ||||
|       // Manage modals | ||||
|   | ||||
| @@ -1,42 +1,60 @@ | ||||
| .promo_achievement_CottonCandyPink { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -424px -277px; | ||||
|   width: 204px; | ||||
|   height: 102px; | ||||
| } | ||||
| .promo_armoire_backgrounds_202003 { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px -277px; | ||||
|   background-position: -445px -184px; | ||||
|   width: 423px; | ||||
|   height: 147px; | ||||
| } | ||||
| .promo_mystery_202003 { | ||||
| .promo_egg_quest { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px -425px; | ||||
|   background-position: 0px -500px; | ||||
|   width: 354px; | ||||
|   height: 147px; | ||||
| } | ||||
| .promo_mystery_202004 { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -355px -500px; | ||||
|   width: 282px; | ||||
|   height: 147px; | ||||
| } | ||||
| .promo_seasonal_shop_spring { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -638px -500px; | ||||
|   width: 162px; | ||||
|   height: 138px; | ||||
| } | ||||
| .promo_spring_2019 { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px -337px; | ||||
|   width: 432px; | ||||
|   height: 162px; | ||||
| } | ||||
| .promo_spring_2020 { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -445px 0px; | ||||
|   width: 429px; | ||||
|   height: 183px; | ||||
| } | ||||
| .promo_spring_potions_2020 { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -433px -337px; | ||||
|   width: 423px; | ||||
|   height: 147px; | ||||
| } | ||||
| .promo_take_this { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -566px -425px; | ||||
|   background-position: -151px -648px; | ||||
|   width: 96px; | ||||
|   height: 69px; | ||||
| } | ||||
| .scene_dailies { | ||||
| .scene_hat_guild { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: 0px 0px; | ||||
|   width: 327px; | ||||
|   height: 276px; | ||||
|   width: 444px; | ||||
|   height: 336px; | ||||
| } | ||||
| .scene_gaining_achievement { | ||||
| .scene_meditation { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -328px 0px; | ||||
|   width: 339px; | ||||
|   height: 210px; | ||||
| } | ||||
| .scene_shanaqui { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png'); | ||||
|   background-position: -283px -425px; | ||||
|   width: 282px; | ||||
|   height: 147px; | ||||
|   background-position: 0px -648px; | ||||
|   width: 150px; | ||||
|   height: 150px; | ||||
| } | ||||
|   | ||||
| @@ -1,222 +1,228 @@ | ||||
| .achievement-alien { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1446px -1628px; | ||||
|   background-position: -1659px -1507px; | ||||
|   width: 24px; | ||||
|   height: 26px; | ||||
| } | ||||
| .achievement-alien2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -813px -1549px; | ||||
|   background-position: -862px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-allYourBase2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1092px -1480px; | ||||
|   background-position: -1153px -1480px; | ||||
|   width: 64px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-alpha2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1372px -1628px; | ||||
|   background-position: -1470px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-aridAuthority2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1027px -1480px; | ||||
|   background-position: -1088px -1480px; | ||||
|   width: 64px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-armor2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1274px -1628px; | ||||
|   background-position: -1372px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-backToBasics2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1549px -1480px; | ||||
|   background-position: -1610px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-bewilder2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1176px -1628px; | ||||
|   background-position: -1274px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-birthday2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1127px -1628px; | ||||
|   background-position: -1225px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-boot2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1078px -1628px; | ||||
|   background-position: -1176px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-bow2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1029px -1628px; | ||||
|   background-position: -1127px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-burnout2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -980px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-cactus2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -931px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-cake2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -882px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-cave2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -833px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-challenge2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -784px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-comment2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -735px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-completedTask2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1500px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-congrats2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -637px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-costumeContest2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -588px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-createdTask2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1451px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-dilatory2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -490px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-dustDevil2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1402px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-dysheartener2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -392px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-fedPet2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1353px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-friends2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -294px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-getwell2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -245px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-goodluck2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -196px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-greeting2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -147px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-guild2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -98px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-habitBirthday2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -49px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-habiticaDay2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: 0px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-hatchedPet2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1304px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-heart2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1597px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-justAddWater2x { | ||||
| .achievement-bugBonanza2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -775px -1480px; | ||||
|   width: 60px; | ||||
|   height: 64px; | ||||
| } | ||||
| .achievement-burnout2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1029px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-cactus2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -980px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-cake2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -931px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-cave2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -882px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-challenge2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -833px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-comment2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -784px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-completedTask2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1512px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-congrats2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -686px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-costumeContest2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -637px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-createdTask2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1463px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-dilatory2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -539px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-dustDevil2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1414px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-dysheartener2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -441px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-fedPet2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1365px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-friends2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -343px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-getwell2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -294px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-goodluck2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -245px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-greeting2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -196px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-guild2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -147px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-habitBirthday2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -98px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-habiticaDay2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -49px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-hatchedPet2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1316px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-heart2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1646px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-justAddWater2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -836px -1480px; | ||||
|   width: 60px; | ||||
|   height: 64px; | ||||
| } | ||||
| .achievement-karaoke-2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1499px -1549px; | ||||
|   background-position: -1548px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-karaoke { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1421px -1628px; | ||||
|   background-position: -1659px -1480px; | ||||
|   width: 24px; | ||||
|   height: 26px; | ||||
| } | ||||
| @@ -228,79 +234,79 @@ | ||||
| } | ||||
| .achievement-lostMasterclasser2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1352px -1549px; | ||||
|   background-position: -1401px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-mindOverMatter2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -836px -1480px; | ||||
|   background-position: -897px -1480px; | ||||
|   width: 60px; | ||||
|   height: 64px; | ||||
| } | ||||
| .achievement-monsterMagus2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1157px -1480px; | ||||
|   background-position: -1218px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-ninja2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1205px -1549px; | ||||
|   background-position: -1254px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-npc2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1156px -1549px; | ||||
|   background-position: -1205px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-nye2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1107px -1549px; | ||||
|   background-position: -1156px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-partyOn2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1058px -1549px; | ||||
|   background-position: -1107px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-partyUp2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1009px -1549px; | ||||
|   background-position: -1058px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-pearlyPro2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -962px -1480px; | ||||
|   background-position: -958px -1480px; | ||||
|   width: 64px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-perfect2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -911px -1549px; | ||||
|   background-position: -960px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-primedForPainting2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1206px -1480px; | ||||
|   background-position: -1267px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-purchasedEquipment2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1255px -1480px; | ||||
|   background-position: -1561px -1480px; | ||||
|   width: 48px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-rat2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -764px -1549px; | ||||
|   background-position: -813px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| @@ -312,31 +318,31 @@ | ||||
| } | ||||
| .achievement-royally-loyal2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -666px -1549px; | ||||
|   background-position: -715px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-seafoam2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -617px -1549px; | ||||
|   background-position: -666px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-shield2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -568px -1549px; | ||||
|   background-position: -617px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-shinySeed2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1647px -1480px; | ||||
|   background-position: -568px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-snowball2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1598px -1480px; | ||||
|   background-position: -1421px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| @@ -348,31 +354,31 @@ | ||||
| } | ||||
| .achievement-stoikalm2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1225px -1628px; | ||||
|   background-position: -1078px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-sun2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -686px -1628px; | ||||
|   background-position: -735px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-sword2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -539px -1628px; | ||||
|   background-position: -588px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-thankyou2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -441px -1628px; | ||||
|   background-position: -490px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-thermometer2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -343px -1628px; | ||||
|   background-position: -392px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| @@ -384,379 +390,379 @@ | ||||
| } | ||||
| .achievement-tree2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1548px -1549px; | ||||
|   background-position: -1597px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-triadbingo2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1450px -1549px; | ||||
|   background-position: -1499px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-ultimate-healer2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1303px -1549px; | ||||
|   background-position: -1352px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-ultimate-mage2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1254px -1549px; | ||||
|   background-position: -1303px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-ultimate-rogue2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -960px -1549px; | ||||
|   background-position: -1009px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-ultimate-warrior2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -862px -1549px; | ||||
|   background-position: -911px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-undeadUndertaker2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -897px -1480px; | ||||
|   background-position: -1023px -1480px; | ||||
|   width: 64px; | ||||
|   height: 56px; | ||||
| } | ||||
| .achievement-unearned2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -715px -1549px; | ||||
|   background-position: -764px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-valentine2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1646px -1549px; | ||||
|   background-position: 0px -1628px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .achievement-wolf2x { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1401px -1549px; | ||||
|   background-position: -1450px -1549px; | ||||
|   width: 48px; | ||||
|   height: 52px; | ||||
| } | ||||
| .background_alpine_slopes { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1278px -296px; | ||||
|   background-position: -1278px -444px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_amid_ancient_ruins { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1278px -444px; | ||||
|   background-position: -1278px -592px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_among_giant_anemones { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1278px -592px; | ||||
|   background-position: -1278px -740px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_among_giant_flowers { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1278px -740px; | ||||
|   background-position: -1278px -888px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .customize-option.background_among_giant_flowers { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1303px -755px; | ||||
|   background-position: -1303px -903px; | ||||
|   width: 60px; | ||||
|   height: 60px; | ||||
| } | ||||
| .background_apple_picking { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1278px -888px; | ||||
|   background-position: -1278px -1036px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_aquarium { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1278px -1036px; | ||||
|   background-position: 0px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_archaeological_dig { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: 0px -1184px; | ||||
|   background-position: -142px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_archery_range { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -142px -1184px; | ||||
|   background-position: -284px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_at_the_docks { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -284px -1184px; | ||||
|   background-position: -426px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_aurora { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -426px -1184px; | ||||
|   background-position: -568px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_autumn_flower_garden { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -568px -1184px; | ||||
|   background-position: -710px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .customize-option.background_autumn_flower_garden { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -593px -1199px; | ||||
|   background-position: -735px -1199px; | ||||
|   width: 60px; | ||||
|   height: 60px; | ||||
| } | ||||
| .background_autumn_forest { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -710px -1184px; | ||||
|   background-position: -852px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_avalanche { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -852px -1184px; | ||||
|   background-position: -994px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_back_alley { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -994px -1184px; | ||||
|   background-position: -1136px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_back_of_giant_beast { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1136px -1184px; | ||||
|   background-position: -1278px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_bamboo_forest { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1278px -1184px; | ||||
|   background-position: -1420px 0px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_bayou { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px 0px; | ||||
|   background-position: -1420px -148px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_beach { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -148px; | ||||
|   background-position: -1420px -296px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_beehive { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -296px; | ||||
|   background-position: -1420px -444px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_bell_tower { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -444px; | ||||
|   background-position: -1420px -592px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_beside_well { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -592px; | ||||
|   background-position: -1420px -740px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_birch_forest { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -740px; | ||||
|   background-position: -1420px -888px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_birthday_party { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -888px; | ||||
|   background-position: -1420px -1036px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_blacksmithy { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -1036px; | ||||
|   background-position: -1420px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_blizzard { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -1184px; | ||||
|   background-position: 0px 0px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_blossoming_desert { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: 0px 0px; | ||||
|   background-position: -142px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_blue { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -142px -1332px; | ||||
|   background-position: -284px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_bridge { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -284px -1332px; | ||||
|   background-position: -426px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_bug_covered_log { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -426px -1332px; | ||||
|   background-position: -568px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_buried_treasure { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -568px -1332px; | ||||
|   background-position: -710px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_butterfly_garden { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -710px -1332px; | ||||
|   background-position: -852px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_champions_colosseum { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -852px -1332px; | ||||
|   background-position: -994px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_cherry_trees { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -994px -1332px; | ||||
|   background-position: -1136px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_chessboard_land { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1136px -1332px; | ||||
|   background-position: -1278px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_clouds { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1278px -1332px; | ||||
|   background-position: -1420px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_coral_reef { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1420px -1332px; | ||||
|   background-position: -1562px 0px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_cornfields { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px 0px; | ||||
|   background-position: -1562px -148px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_cozy_barn { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -148px; | ||||
|   background-position: -1562px -296px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_cozy_bedroom { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -296px; | ||||
|   background-position: -1562px -444px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_cozy_library { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -444px; | ||||
|   background-position: -1562px -592px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_creepy_castle { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -592px; | ||||
|   background-position: -1562px -740px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_crosscountry_ski_trail { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -740px; | ||||
|   background-position: -1562px -888px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_crystal_cave { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -888px; | ||||
|   background-position: -1562px -1036px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_dark_deep { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -1036px; | ||||
|   background-position: -1562px -1184px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_deep_mine { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -1184px; | ||||
|   background-position: -1562px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_deep_sea { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -1562px -1332px; | ||||
|   background-position: 0px -1480px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_desert_dunes { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: 0px -1480px; | ||||
|   background-position: -142px -1480px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_desert_with_snow { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -142px -1480px; | ||||
|   background-position: -284px -1480px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_dilatory_castle { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -284px -1480px; | ||||
|   background-position: -426px -1480px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_dilatory_city { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: -426px -1480px; | ||||
|   background-position: 0px -1332px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
| .background_dilatory_ruins { | ||||
|   background-image: url('~@/assets/images/sprites/spritesmith-main-0.png'); | ||||
|   background-position: 0px -1332px; | ||||
|   background-position: -1278px -296px; | ||||
|   width: 141px; | ||||
|   height: 147px; | ||||
| } | ||||
|   | ||||
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 19 KiB | 
| Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.7 KiB | 
| Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 7.8 KiB | 
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB | 
| Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.8 KiB | 
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 68 KiB | 
| Before Width: | Height: | Size: 474 KiB After Width: | Height: | Size: 473 KiB | 
| Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 118 KiB | 
| Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 118 KiB | 
| Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 193 KiB | 
| Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 426 KiB | 
| Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 214 KiB | 
| Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 166 KiB | 
| Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 144 KiB | 
| Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 139 KiB | 
| Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 148 KiB | 
| Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 163 KiB | 
| Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 147 KiB | 
| Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 147 KiB | 
| Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 157 KiB | 
| Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB | 
| Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 184 KiB | 
| Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 166 KiB | 
| Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 166 KiB | 
| Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 87 KiB | 
| Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 140 KiB | 
| @@ -2,11 +2,11 @@ | ||||
| // possible values are: normal, fall, habitoween, thanksgiving, winter, nye, birthday, valentines, spring, summer | ||||
| // more to be added on future seasons | ||||
|  | ||||
| $npc_market_flavor: 'normal'; | ||||
| $npc_quests_flavor: 'normal'; | ||||
| $npc_seasonal_flavor: 'normal'; | ||||
| $npc_timetravelers_flavor: 'normal'; | ||||
| $npc_tavern_flavor: 'normal'; | ||||
| $npc_market_flavor: 'spring'; | ||||
| $npc_quests_flavor: 'spring'; | ||||
| $npc_seasonal_flavor: 'spring'; | ||||
| $npc_timetravelers_flavor: 'spring'; | ||||
| $npc_tavern_flavor: 'spring'; | ||||
|  | ||||
| $restingToolbarHeight: 40px; | ||||
| $menuToolbarHeight: 56px; | ||||
|   | ||||
| @@ -225,30 +225,30 @@ export default { | ||||
|     classGear (heroClass) { | ||||
|       if (heroClass === 'rogue') { | ||||
|         return { | ||||
|           armor: 'armor_rogue_5', | ||||
|           head: 'head_rogue_5', | ||||
|           shield: 'shield_rogue_6', | ||||
|           weapon: 'weapon_rogue_6', | ||||
|           armor: 'armor_special_spring2020Rogue', | ||||
|           head: 'head_special_spring2020Rogue', | ||||
|           shield: 'shield_special_spring2020Rogue', | ||||
|           weapon: 'weapon_special_spring2020Rogue', | ||||
|         }; | ||||
|       } if (heroClass === 'wizard') { | ||||
|         return { | ||||
|           armor: 'armor_wizard_5', | ||||
|           head: 'head_wizard_5', | ||||
|           weapon: 'weapon_wizard_6', | ||||
|           armor: 'armor_special_spring2020Mage', | ||||
|           head: 'head_special_spring2020Mage', | ||||
|           weapon: 'weapon_special_spring2020Mage', | ||||
|         }; | ||||
|       } if (heroClass === 'healer') { | ||||
|         return { | ||||
|           armor: 'armor_healer_5', | ||||
|           head: 'head_healer_5', | ||||
|           shield: 'shield_healer_5', | ||||
|           weapon: 'weapon_healer_6', | ||||
|           armor: 'armor_special_spring2020Healer', | ||||
|           head: 'head_special_spring2020Healer', | ||||
|           shield: 'shield_special_spring2020Healer', | ||||
|           weapon: 'weapon_special_spring2020Healer', | ||||
|         }; | ||||
|       } | ||||
|       return { | ||||
|         armor: 'armor_warrior_5', | ||||
|         head: 'head_warrior_5', | ||||
|         shield: 'shield_warrior_5', | ||||
|         weapon: 'weapon_warrior_6', | ||||
|         armor: 'armor_special_spring2020Warrior', | ||||
|         head: 'head_special_spring2020Warrior', | ||||
|         shield: 'shield_special_spring2020Warrior', | ||||
|         weapon: 'weapon_special_spring2020Warrior', | ||||
|       }; | ||||
|     }, | ||||
|     selectionBox (selectedClass, heroClass) { | ||||
|   | ||||
| @@ -11,15 +11,17 @@ | ||||
|         @click="close()" | ||||
|       > | ||||
|         <div | ||||
|           v-once | ||||
|           class="svg-icon" | ||||
|           v-html="icons.close" | ||||
|           v-once | ||||
|         ></div> | ||||
|       </div> | ||||
|       <h2 | ||||
|         class="mt-3 mb-4" | ||||
|         v-once | ||||
|       >{{ $t('foundNewItems') }}</h2> | ||||
|         class="mt-3 mb-4" | ||||
|       > | ||||
|         {{ $t('foundNewItems') }} | ||||
|       </h2> | ||||
|       <div class="d-flex justify-content-center"> | ||||
|         <div | ||||
|           class="item-box ml-auto mr-3" | ||||
| @@ -33,17 +35,21 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|       <p | ||||
|         v-once | ||||
|         class="mt-4" | ||||
|         v-once | ||||
|       >{{ $t('foundNewItemsExplanation') }}</p> | ||||
|       > | ||||
|         {{ $t('foundNewItemsExplanation') }} | ||||
|       </p> | ||||
|       <p | ||||
|         class="strong mb-4" | ||||
|         v-once | ||||
|       >{{ $t('foundNewItemsCTA') }}</p> | ||||
|         class="strong mb-4" | ||||
|       > | ||||
|         {{ $t('foundNewItemsCTA') }} | ||||
|       </p> | ||||
|       <button | ||||
|         v-once | ||||
|         class="btn btn-primary mb-2" | ||||
|         @click="toInventory()" | ||||
|         v-once | ||||
|       > | ||||
|         {{ $t('letsgo') }} | ||||
|       </button> | ||||
|   | ||||
| @@ -186,7 +186,7 @@ export default { | ||||
|         return this.overrideTopPadding; | ||||
|       } | ||||
|  | ||||
|       let val = '27px'; | ||||
|       let val = '24px'; | ||||
|  | ||||
|       if (!this.avatarOnly) { | ||||
|         if (this.member.items.currentPet) val = '24px'; | ||||
|   | ||||
| @@ -475,7 +475,7 @@ export default { | ||||
|       return this.$store.state.memberModalOptions.challengeId; | ||||
|     }, | ||||
|     sortedMembers () { | ||||
|       let sortedMembers = this.members; | ||||
|       let sortedMembers = this.members.slice(); // shallow clone to avoid infinite loop | ||||
|  | ||||
|       if (!isEmpty(this.sortOption)) { | ||||
|         // Use the memberlist filtered by searchTerm | ||||
| @@ -483,7 +483,13 @@ export default { | ||||
|           // If members are to be sorted by name, use localeCompare for case- | ||||
|           // insensitive sort | ||||
|           sortedMembers.sort( | ||||
|             (a, b) => a.profile.name.localeCompare(b.profile.name), | ||||
|             (a, b) => { | ||||
|               if (this.sortOption.direction === 'desc') { | ||||
|                 return b.profile.name.localeCompare(a.profile.name); | ||||
|               } | ||||
|  | ||||
|               return a.profile.name.localeCompare(b.profile.name); | ||||
|             }, | ||||
|           ); | ||||
|         } else { | ||||
|           sortedMembers = orderBy( | ||||
|   | ||||
| @@ -30,6 +30,7 @@ | ||||
|       </div> | ||||
|       <div | ||||
|         v-if="hasParty" | ||||
|         ref="partyMembersDiv" | ||||
|         v-resize="1500" | ||||
|         class="party-members d-flex" | ||||
|         @resized="setPartyMembersWidth($event)" | ||||
| @@ -153,6 +154,15 @@ export default { | ||||
|       inviteModalGroupType: undefined, | ||||
|     }; | ||||
|   }, | ||||
|   watch: { | ||||
|     hideHeader () { | ||||
|       this.$nextTick(() => { | ||||
|         if (this.$refs.partyMembersDiv) { | ||||
|           this.setPartyMembersWidth({ width: this.$refs.partyMembersDiv.clientWidth }); | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       user: 'user:data', | ||||
| @@ -170,7 +180,30 @@ export default { | ||||
|       return Math.floor(this.currentWidth / 140) + 1; | ||||
|     }, | ||||
|     sortedPartyMembers () { | ||||
|       return orderBy(this.partyMembers, [this.user.party.order], [this.user.party.orderAscending]); | ||||
|       let sortedMembers = this.partyMembers.slice(); // shallow clone to avoid infinite loop | ||||
|       const { order, orderAscending } = this.user.party; | ||||
|  | ||||
|       if (order === 'profile.name') { | ||||
|         // If members are to be sorted by name, use localeCompare for case- | ||||
|         // insensitive sort | ||||
|         sortedMembers.sort( | ||||
|           (a, b) => { | ||||
|             if (orderAscending === 'desc') { | ||||
|               return b.profile.name.localeCompare(a.profile.name); | ||||
|             } | ||||
|  | ||||
|             return a.profile.name.localeCompare(b.profile.name); | ||||
|           }, | ||||
|         ); | ||||
|       } else { | ||||
|         sortedMembers = orderBy( | ||||
|           sortedMembers, | ||||
|           [order], | ||||
|           [orderAscending], | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       return sortedMembers; | ||||
|     }, | ||||
|     hideHeader () { | ||||
|       return ['groupPlan', 'privateMessages'].includes(this.$route.name); | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|       <div class="username-notification-title"> | ||||
|         {{ $t('setUsernameNotificationTitle') }} | ||||
|       </div> | ||||
|       <div>{{ $t('setUsernameNotificationBody') }}</div> | ||||
|       <div>{{ $t('changeUsernameDisclaimer') }}</div> | ||||
|       <div class="current-username-container mx-auto"> | ||||
|         <label class="font-weight-bold">{{ $t('currentUsername') + " " }}</label> | ||||
|         <label>@</label> | ||||
|   | ||||
| @@ -45,9 +45,9 @@ | ||||
|         v-if="username" | ||||
|         class="username" | ||||
|       >@{{ username }}</span> <span | ||||
|       v-if="lastMessageDate" | ||||
|       class="time" | ||||
|     >• | ||||
|         v-if="lastMessageDate" | ||||
|         class="time" | ||||
|       >• | ||||
|         {{ lastMessageDate | timeAgo }} | ||||
|       </span> | ||||
|     </span> | ||||
| @@ -56,7 +56,6 @@ | ||||
|       <div class="messagePreview"> | ||||
|         {{ lastMessageText }} | ||||
|       </div> | ||||
|  | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|   | ||||
| @@ -310,6 +310,14 @@ const NOTIFICATIONS = { | ||||
|       achievement: 'rosyOutlook', // defined manually until the server sends all the necessary data | ||||
|     }, | ||||
|   }, | ||||
|   ACHIEVEMENT_BUG_BONANZA: { | ||||
|     achievement: true, | ||||
|     label: $t => `${$t('achievement')}: ${$t('achievementBugBonanza')}`, | ||||
|     modalId: 'generic-achievement', | ||||
|     data: { | ||||
|       achievement: 'bugBonanza', // defined manually until the server sends all the necessary data | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export default { | ||||
| @@ -368,7 +376,7 @@ export default { | ||||
|       'ACHIEVEMENT_MOUNT_MASTER', 'ACHIEVEMENT_TRIAD_BINGO', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY', | ||||
|       'ACHIEVEMENT_MONSTER_MAGUS', 'ACHIEVEMENT_UNDEAD_UNDERTAKER', 'ACHIEVEMENT_PRIMED_FOR_PAINTING', | ||||
|       'ACHIEVEMENT_PEARLY_PRO', 'ACHIEVEMENT_TICKLED_PINK', 'ACHIEVEMENT_ROSY_OUTLOOK', 'ACHIEVEMENT', | ||||
|       'ONBOARDING_COMPLETE', 'FIRST_DROPS', | ||||
|       'ONBOARDING_COMPLETE', 'FIRST_DROPS', 'ACHIEVEMENT_BUG_BONANZA', | ||||
|     ].forEach(type => { | ||||
|       handledNotifications[type] = true; | ||||
|     }); | ||||
| @@ -782,6 +790,7 @@ export default { | ||||
|           case 'ACHIEVEMENT_PEARLY_PRO': | ||||
|           case 'ACHIEVEMENT_TICKLED_PINK': | ||||
|           case 'ACHIEVEMENT_ROSY_OUTLOOK': | ||||
|           case 'ACHIEVEMENT_BUG_BONANZA': | ||||
|           case 'GENERIC_ACHIEVEMENT': | ||||
|             this.showNotificationWithModal(notification); | ||||
|             break; | ||||
|   | ||||
| @@ -833,6 +833,7 @@ export default { | ||||
|     }, | ||||
|     async deleteSocialAuth (network) { | ||||
|       await axios.delete(`/api/v4/user/auth/social/${network.key}`); | ||||
|       this.user.auth[network.key] = {}; | ||||
|       this.text(this.$t('detachedSocial', { network: network.name })); | ||||
|     }, | ||||
|     async socialAuth (network) { | ||||
|   | ||||
| @@ -123,7 +123,6 @@ | ||||
|                 class="btn btn-block btn-info sign-up" | ||||
|                 :disabled="signupFormInvalid" | ||||
|                 type="submit" | ||||
|                 @click="register()" | ||||
|               > | ||||
|                 {{ $t('signup') }} | ||||
|               </button> | ||||
|   | ||||
| @@ -167,7 +167,7 @@ | ||||
|             <div class="d-inline-flex"> | ||||
|               <div | ||||
|                 v-if="isUser" | ||||
|                 v-b-tooltip.hover.bottom="$t(`${task.collapseChecklist | ||||
|                 v-b-tooltip.hover.right="$t(`${task.collapseChecklist | ||||
|                   ? 'expand': 'collapse'}Checklist`)" | ||||
|                 class="collapse-checklist d-flex align-items-center expand-toggle" | ||||
|                 :class="{open: !task.collapseChecklist}" | ||||
|   | ||||
| @@ -138,7 +138,8 @@ | ||||
|             class="inline-edit-input checklist-item form-control" | ||||
|             type="text" | ||||
|             :placeholder="$t('newChecklistItem')" | ||||
|             @keydown.enter="addChecklistItem($event)" | ||||
|             @keypress.enter="setHasPossibilityOfIMEConversion(false)" | ||||
|             @keyup.enter="addChecklistItem($event)" | ||||
|           > | ||||
|         </div> | ||||
|         <div | ||||
| @@ -1222,6 +1223,7 @@ export default { | ||||
|         per: 'perception', | ||||
|       }, | ||||
|       calendarHighlights: { dates: [new Date()] }, | ||||
|       hasPossibilityOfIMEConversion: true, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
| @@ -1384,7 +1386,12 @@ export default { | ||||
|       sorting.splice(data.newIndex, 0, movingItem); | ||||
|       this.task.checklist = sorting; | ||||
|     }, | ||||
|     setHasPossibilityOfIMEConversion (bool) { | ||||
|       this.hasPossibilityOfIMEConversion = bool; | ||||
|     }, | ||||
|     addChecklistItem (e) { | ||||
|       if (e) e.preventDefault(); | ||||
|       if (this.hasPossibilityOfIMEConversion) return; | ||||
|       const checkListItem = { | ||||
|         id: uuid.v4(), | ||||
|         text: this.newChecklistItem, | ||||
| @@ -1394,7 +1401,7 @@ export default { | ||||
|       // @TODO: managing checklist separately to help with sorting on the UI | ||||
|       this.checklist.push(checkListItem); | ||||
|       this.newChecklistItem = null; | ||||
|       if (e) e.preventDefault(); | ||||
|       this.setHasPossibilityOfIMEConversion(true); | ||||
|     }, | ||||
|     removeChecklistItem (i) { | ||||
|       this.task.checklist.splice(i, 1); | ||||
|   | ||||
| @@ -348,6 +348,7 @@ | ||||
|                 ></div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|             <div | ||||
|               v-if="achievementsCategories[key].number > 5" | ||||
|               class="btn btn-flat btn-show-more" | ||||
| @@ -358,7 +359,6 @@ | ||||
|                 $t('showAllAchievements', {category: $t(key+'Achievs')}) | ||||
|               }} | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <hr class="col-12"> | ||||
|   | ||||