mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-13 12:47:28 +01:00
Compare commits
85 Commits
v5.35.1
...
fiz/update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cf181c95c | ||
|
|
cd9615d731 | ||
|
|
1df8f12541 | ||
|
|
6c766aeaff | ||
|
|
876d5a67d6 | ||
|
|
3078af8f2a | ||
|
|
dad1440138 | ||
|
|
12773d539e | ||
|
|
ae4130b108 | ||
|
|
ad0614282e | ||
|
|
5a7704aed7 | ||
|
|
2feadd6125 | ||
|
|
efe0b3cd9e | ||
|
|
96731da380 | ||
|
|
0c5dd5d8b5 | ||
|
|
2f943a22e6 | ||
|
|
666184d7e4 | ||
|
|
17d22dda3f | ||
|
|
d1a18c121d | ||
|
|
836d7f3991 | ||
|
|
ace9c3c46a | ||
|
|
068640311e | ||
|
|
f26d2a59ae | ||
|
|
03c7e9172e | ||
|
|
6fdc072ec3 | ||
|
|
e68661c04b | ||
|
|
4f567592ea | ||
|
|
63c9b7a894 | ||
|
|
eaec39188e | ||
|
|
ba6940eb81 | ||
|
|
f8a3e4d673 | ||
|
|
2727da6f6c | ||
|
|
fa97852e38 | ||
|
|
2c7da25a25 | ||
|
|
9a072e3e76 | ||
|
|
823b339d27 | ||
|
|
fe98d9485d | ||
|
|
407e1bb560 | ||
|
|
98a6535dc3 | ||
|
|
9948e8ee44 | ||
|
|
bce07ec357 | ||
|
|
039af36344 | ||
|
|
836807aa1e | ||
|
|
ebbcbef6d5 | ||
|
|
ccc6c9867f | ||
|
|
1a7ba0a84c | ||
|
|
20d31ed8c8 | ||
|
|
39ff6cbe05 | ||
|
|
1bf2efa885 | ||
|
|
4b45a6389c | ||
|
|
5ba7d2395e | ||
|
|
972f23e235 | ||
|
|
9f599b0c8e | ||
|
|
b937c2df0b | ||
|
|
9c4396027a | ||
|
|
2bab20d032 | ||
|
|
cb2ee670e3 | ||
|
|
b65d23d535 | ||
|
|
007cdf0ca2 | ||
|
|
1e4799bac6 | ||
|
|
47222445ad | ||
|
|
126b382da1 | ||
|
|
ec78831a81 | ||
|
|
9bfb2afd9c | ||
|
|
389124b83f | ||
|
|
eb25330296 | ||
|
|
29892ff5e3 | ||
|
|
99a31b322a | ||
|
|
1884c6c751 | ||
|
|
9456477953 | ||
|
|
e3512a2bdd | ||
|
|
6ce3f84458 | ||
|
|
484c3cbac8 | ||
|
|
c199beaf8c | ||
|
|
553aa01c25 | ||
|
|
8d1b10e458 | ||
|
|
0eaee9b1e4 | ||
|
|
41bbc475ab | ||
|
|
d6e03c765e | ||
|
|
dd6503d5ef | ||
|
|
36e5f39d7c | ||
|
|
d48e4a664f | ||
|
|
661b30e807 | ||
|
|
026e819271 | ||
|
|
1fab19acf4 |
@@ -1,11 +0,0 @@
|
||||
import gulp from 'gulp';
|
||||
import nodemon from 'gulp-nodemon';
|
||||
|
||||
import pkg from '../package.json';
|
||||
|
||||
gulp.task('nodemon', done => {
|
||||
nodemon({
|
||||
script: pkg.main,
|
||||
});
|
||||
done();
|
||||
});
|
||||
@@ -49,12 +49,6 @@ function integrationTestCommand (testDir) {
|
||||
}
|
||||
|
||||
/* Test task definitions */
|
||||
gulp.task('test:nodemon', gulp.series(done => {
|
||||
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
|
||||
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
|
||||
done();
|
||||
}, 'nodemon'));
|
||||
|
||||
gulp.task('test:prepare:mongo', cb => {
|
||||
const mongooseOptions = getDefaultConnectionOptions();
|
||||
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
|
||||
|
||||
@@ -21,7 +21,6 @@ if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-e
|
||||
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
|
||||
|
||||
Submodule habitica-images updated: 1c6f7d65d7...cf18a04339
@@ -37,7 +37,7 @@ let consoleStamp = require('console-stamp');
|
||||
consoleStamp(console);
|
||||
|
||||
// Initialize configuration
|
||||
require('../../website/server/libs/api-v3/setupNconf')();
|
||||
require('../../website/server/libs/api-v3/setupNconf').default();
|
||||
|
||||
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
||||
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
||||
|
||||
@@ -32,7 +32,7 @@ let moment = require('moment');
|
||||
consoleStamp(console);
|
||||
|
||||
// Initialize configuration
|
||||
require('../../website/server/libs/api-v3/setupNconf')();
|
||||
require('../../website/server/libs/api-v3/setupNconf').default();
|
||||
|
||||
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
||||
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
||||
|
||||
@@ -6,7 +6,7 @@ require('@babel/register'); // eslint-disable-line import/no-extraneous-dependen
|
||||
function setUpServer () {
|
||||
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
|
||||
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
|
||||
const setupNconf = require('../website/server/libs/setupNconf'); // eslint-disable-line global-require
|
||||
const setupNconf = require('../website/server/libs/setupNconf').default; // eslint-disable-line global-require
|
||||
|
||||
setupNconf();
|
||||
|
||||
|
||||
937
package-lock.json
generated
937
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.35.1",
|
||||
"version": "5.38.2",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -38,7 +38,6 @@
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-filter": "^7.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"helmet": "^4.6.0",
|
||||
@@ -50,12 +49,11 @@
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^7.8.3",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
"nodemon": "^2.0.20",
|
||||
"on-headers": "^1.0.2",
|
||||
"passport": "^0.5.3",
|
||||
"passport-facebook": "^3.0.0",
|
||||
@@ -100,23 +98,22 @@
|
||||
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
||||
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
||||
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
||||
"test:nodemon": "gulp test:nodemon",
|
||||
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
||||
"sprites": "gulp sprites:compile",
|
||||
"client:dev": "cd website/client && npm run serve",
|
||||
"client:build": "cd website/client && npm run build",
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "gulp nodemon",
|
||||
"start": "node --watch ./website/server/index.js",
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"debug": "node --watch --inspect ./website/server/index.js",
|
||||
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2404 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2404 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.8.2",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
|
||||
@@ -71,15 +71,14 @@ async function deleteHabiticaData (user, email) {
|
||||
}
|
||||
|
||||
async function processEmailAddress (email) {
|
||||
const emailRegex = new RegExp(`^${email}$`, 'i');
|
||||
const localUsers = await User.find(
|
||||
{ 'auth.local.email': emailRegex },
|
||||
{ 'auth.local.email': email },
|
||||
{ _id: 1, apiToken: 1, auth: 1 },
|
||||
).exec();
|
||||
|
||||
const socialUsers = await User.find(
|
||||
{
|
||||
'auth.local.email': { $not: emailRegex },
|
||||
'auth.local.email': { $ne: email },
|
||||
$or: [
|
||||
{ 'auth.facebook.emails.value': email },
|
||||
{ 'auth.google.emails.value': email },
|
||||
|
||||
@@ -8,7 +8,17 @@ const TASK_VALUE_CHANGE_FACTOR = 0.9747;
|
||||
const MIN_TASK_VALUE = -47.27;
|
||||
|
||||
async function updateTeamTasks (team) {
|
||||
if (team.purchased.plan.dateTerminated) {
|
||||
const dateTerminated = new Date(team.purchased.plan.dateTerminated);
|
||||
if (dateTerminated < new Date()) {
|
||||
team.purchased.plan.customerId = undefined;
|
||||
team.markModified('purchased.plan');
|
||||
return team.save();
|
||||
}
|
||||
}
|
||||
|
||||
const toSave = [];
|
||||
|
||||
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
|
||||
|
||||
if (!teamLeader) { // why would this happen?
|
||||
@@ -93,12 +103,7 @@ async function updateTeamTasks (team) {
|
||||
export default async function processTeamsCron () {
|
||||
const activeTeams = await Group.find({
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
}).exec();
|
||||
}, { cron: 1, leader: 1, purchased: 1 }).exec();
|
||||
|
||||
const cronPromises = activeTeams.map(updateTeamTasks);
|
||||
return Promise.all(cronPromises);
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { recoverCron, cron } from '../../../../website/server/libs/cron';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import common from '../../../../website/common';
|
||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
|
||||
// const scoreTask = common.ops.scoreTask;
|
||||
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
const pathToCronLib = '../../../../website/server/libs/cron';
|
||||
|
||||
@@ -1200,7 +1209,7 @@ describe('cron', async () => {
|
||||
it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
@@ -1212,7 +1221,7 @@ describe('cron', async () => {
|
||||
it('does not increment perfect day achievement if no due dailies', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
@@ -1224,7 +1233,7 @@ describe('cron', async () => {
|
||||
it('gives perfect day buff if all (at least 1) due dailies were completed', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
@@ -1242,7 +1251,7 @@ describe('cron', async () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
@@ -1259,7 +1268,7 @@ describe('cron', async () => {
|
||||
it('clears buffs if user does not have a perfect day (no due dailys)', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
@@ -1488,78 +1497,6 @@ describe('cron', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifications', async () => {
|
||||
it('adds a user notification', async () => {
|
||||
const mpBefore = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
|
||||
daysMissed = 1;
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore,
|
||||
mp: user.stats.mp - mpBefore,
|
||||
});
|
||||
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
|
||||
it('condenses multiple notifications into one', async () => {
|
||||
const mpBefore1 = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
|
||||
daysMissed = 1;
|
||||
const hpBefore1 = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore1,
|
||||
mp: user.stats.mp - mpBefore1,
|
||||
});
|
||||
|
||||
const notifsBefore2 = user.notifications.length;
|
||||
const hpBefore2 = user.stats.hp;
|
||||
const mpBefore2 = user.stats.mp;
|
||||
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length - notifsBefore2).to.equal(0);
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
|
||||
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
|
||||
});
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('private messages', async () => {
|
||||
let lastMessageId;
|
||||
|
||||
@@ -1606,7 +1543,7 @@ describe('cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.notifications.length).to.be.greaterThan(1);
|
||||
expect(user.notifications.length).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
@@ -1820,64 +1757,258 @@ describe('cron', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('recoverCron', async () => {
|
||||
let locals; let status; let
|
||||
execStub;
|
||||
describe('cron wrapper', () => {
|
||||
let res; let
|
||||
req;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
execStub = sandbox.stub();
|
||||
sandbox.stub(User, 'findOne').returns({ exec: execStub });
|
||||
|
||||
status = { times: 0 };
|
||||
locals = {
|
||||
user: new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@example.com',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analytics;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('throws an error if user cannot be found', async () => {
|
||||
execStub.returns(Promise.resolve(null));
|
||||
it('calls next when user is not attached', async () => {
|
||||
res.locals.user = null;
|
||||
await cronWrapper(req, res);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', async () => {
|
||||
await cronWrapper(req, res);
|
||||
});
|
||||
|
||||
it('should clear todos older than 30 days for free users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.not.exist;
|
||||
});
|
||||
|
||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await Promise.all([task.save(), user.save()]);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.exist;
|
||||
});
|
||||
|
||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.not.exist;
|
||||
});
|
||||
|
||||
it('should call next if user was not modified after cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
});
|
||||
|
||||
it('runs cron if previous cron was incomplete', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
});
|
||||
|
||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
});
|
||||
|
||||
it('does damage for missing dailies', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const updatedUser = await User.findOne({ _id: user._id });
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('updates tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
await Promise.all([todo.save(), user.save()]);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
});
|
||||
|
||||
it('updates large number of tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
const start = new Date();
|
||||
const saves = [todo.save(), user.save()];
|
||||
for (let i = 0; i < 200; i += 1) {
|
||||
const newTodo = generateTodo(user);
|
||||
newTodo.value = i;
|
||||
saves.push(newTodo.save());
|
||||
}
|
||||
await Promise.all(saves);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const duration = new Date() - start;
|
||||
expect(duration).to.be.lessThan(1000);
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(moment(start).isSame(user.lastCron, 'day'));
|
||||
expect(moment(start).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
});
|
||||
|
||||
it('fails entire cron if one task is failing', async () => {
|
||||
const lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
user.lastCron = lastCron;
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
const badTodo = generateTodo(user);
|
||||
badTodo.text = 'bad todo';
|
||||
badTodo.attribute = 'bad';
|
||||
await Promise.all([badTodo.save({ validateBeforeSave: false }), todo.save(), user.save()]);
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when user cannot be found');
|
||||
await cronWrapper(req, res);
|
||||
} catch (err) {
|
||||
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
|
||||
expect(err).to.exist;
|
||||
}
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(moment(lastCron).isSame(user.lastCron, 'day'));
|
||||
expect(todoFound.value).to.be.equal(todoValueBefore);
|
||||
});
|
||||
|
||||
it('applies quest progress', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
|
||||
const questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
|
||||
const party = new Group({
|
||||
type: 'party',
|
||||
name: generateUUID(),
|
||||
leader: user._id,
|
||||
});
|
||||
party.quest.members[user._id] = true;
|
||||
party.quest.key = questKey;
|
||||
await party.save();
|
||||
|
||||
user.party._id = party._id;
|
||||
await user.save();
|
||||
|
||||
party.startQuest(user);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const updatedUser = await User.findOne({ _id: user._id });
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('cronSignature less than 5 minutes ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
try {
|
||||
await cronWrapper(req, res);
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
}
|
||||
});
|
||||
|
||||
it('increases status.times count and reruns up to 4 times', async () => {
|
||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
||||
execStub.onCall(4).returns(Promise.resolve({ _cronSignature: 'NOT_RUNNING' }));
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await recoverCron(status, locals);
|
||||
|
||||
expect(status.times).to.eql(4);
|
||||
expect(locals.user).to.eql({ _cronSignature: 'NOT_RUNNING' });
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
});
|
||||
|
||||
it('throws an error if recoverCron runs 5 times', async () => {
|
||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
||||
it('cron should not run more than once', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when recoverCron runs 5 times');
|
||||
} catch (err) {
|
||||
expect(status.times).to.eql(5);
|
||||
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
||||
}
|
||||
const result = await Promise.allSettled([
|
||||
cronWrapper(req, res),
|
||||
cronWrapper(req, res),
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const runResult = await cronWrapper(req, res);
|
||||
if (runResult !== null) {
|
||||
reject(new Error('cron ran more than once'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}, 200);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(result.filter(r => r.status === 'fulfilled')).to.have.lengthOf(2);
|
||||
expect(result.filter(r => r.status === 'rejected')).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
|
||||
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
||||
@@ -29,22 +28,4 @@ describe('mongodb', () => {
|
||||
expect(string).to.equal('mongodb://hostname:3030');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConnectionOptions', () => {
|
||||
it('returns development config when IS_PROD is false', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
|
||||
it('returns production config when IS_PROD is true', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { authWithHeaders as authWithHeadersFactory } from '../../../../website/server/middlewares/auth';
|
||||
|
||||
const authPath = '../../../../website/server/middlewares/auth';
|
||||
|
||||
describe('auth middleware', () => {
|
||||
let res; let req; let
|
||||
@@ -16,6 +19,7 @@ describe('auth middleware', () => {
|
||||
|
||||
describe('auth with headers', () => {
|
||||
it('allows to specify a list of user field that we do not want to load', done => {
|
||||
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||
const authWithHeaders = authWithHeadersFactory({
|
||||
userFieldsToExclude: ['items'],
|
||||
});
|
||||
@@ -35,6 +39,7 @@ describe('auth middleware', () => {
|
||||
});
|
||||
|
||||
it('makes sure some fields are always included', done => {
|
||||
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||
const authWithHeaders = authWithHeadersFactory({
|
||||
userFieldsToExclude: [
|
||||
'items', 'auth.timestamps',
|
||||
@@ -60,5 +65,57 @@ describe('auth middleware', () => {
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('errors with InvalidCredentialsError and code when token is wrong', done => {
|
||||
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||
const authWithHeaders = authWithHeadersFactory({ userFieldsToExclude: [] });
|
||||
|
||||
req.headers['x-api-user'] = user._id;
|
||||
req.headers['x-api-key'] = 'totally-wrong-token';
|
||||
|
||||
authWithHeaders(req, res, err => {
|
||||
expect(err).to.exist;
|
||||
expect(err.name).to.equal('InvalidCredentialsError');
|
||||
expect(err.code).to.equal('invalid_credentials');
|
||||
expect(err.message).to.equal(res.t('invalidCredentials'));
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when ENFORCE_CLIENT_HEADER is true', () => {
|
||||
let authFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(nconf, 'get').withArgs('ENFORCE_CLIENT_HEADER').returns('true');
|
||||
authFactory = requireAgain(authPath).authWithHeaders;
|
||||
});
|
||||
|
||||
it('errors with missingClientHeader when x-client header is not present', done => {
|
||||
const authWithHeaders = authFactory({ userFieldsToExclude: [] });
|
||||
|
||||
req.headers['x-api-user'] = user._id;
|
||||
req.headers['x-api-key'] = user;
|
||||
authWithHeaders(req, res, err => {
|
||||
expect(err).to.exist;
|
||||
expect(err.name).to.equal('BadRequest');
|
||||
expect(err.message).to.equal(res.t('missingClientHeader'));
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows request to pass when x-client header is present', done => {
|
||||
const authWithHeaders = authFactory({ userFieldsToExclude: [] });
|
||||
|
||||
req.headers['x-api-user'] = user._id;
|
||||
req.headers['x-api-key'] = user.apiToken;
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
authWithHeaders(req, res, err => {
|
||||
if (err) return done(err);
|
||||
expect(res.locals.user).to.exist;
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
197
test/api/unit/middlewares/blocker.test.js
Normal file
197
test/api/unit/middlewares/blocker.test.js
Normal file
@@ -0,0 +1,197 @@
|
||||
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';
|
||||
import { model as Blocker } from '../../../../website/server/models/blocker';
|
||||
|
||||
function checkIPBlockedErrorThrown (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 checkClientBlockedErrorThrown (next) {
|
||||
expect(next).to.have.been.calledOnce;
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(apiError('clientBlocked'));
|
||||
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('Blocker middleware', () => {
|
||||
const pathToBlocker = '../../../../website/server/middlewares/blocker';
|
||||
|
||||
let res; let req; let next;
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
describe('Blocking IPs', () => {
|
||||
it('is disabled when the env var is not defined', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(undefined);
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(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 attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(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 attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('does not throw when the ip does not match', () => {
|
||||
req.ip = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.2');
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('does not throw when the blocker IP does not match', async () => {
|
||||
req.ip = '192.168.1.1';
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
if (event === 'change') {
|
||||
callback({ operation: 'add', blocker: { type: 'ipaddress', area: 'full', value: '192.168.1.2' } });
|
||||
}
|
||||
},
|
||||
});
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('does not throw when a client is blocked', async () => {
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
if (event === 'change') {
|
||||
callback({ operation: 'add', blocker: { type: 'client', area: 'full', value: '192.168.1.1' } });
|
||||
}
|
||||
},
|
||||
});
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('throws when the blocker IP is blocked', async () => {
|
||||
req.ip = '192.168.1.1';
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
if (event === 'change') {
|
||||
callback({ operation: 'add', blocker: { type: 'ipaddress', area: 'full', value: '192.168.1.1' } });
|
||||
}
|
||||
},
|
||||
});
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkIPBlockedErrorThrown(next);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Blocking clients', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('');
|
||||
req.headers['x-client'] = 'test-client';
|
||||
});
|
||||
it('is disabled when no clients are blocked', () => {
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('does not throw when the client does not match', async () => {
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
if (event === 'change') {
|
||||
callback({ operation: 'add', blocker: { type: 'client', area: 'full', value: 'another-client' } });
|
||||
}
|
||||
},
|
||||
});
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('throws when the client is blocked', async () => {
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
if (event === 'change') {
|
||||
callback({ operation: 'add', blocker: { type: 'client', area: 'full', value: 'test-client' } });
|
||||
}
|
||||
},
|
||||
});
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkClientBlockedErrorThrown(next);
|
||||
});
|
||||
|
||||
it('does not throw when an ip is blocked', async () => {
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
if (event === 'change') {
|
||||
callback({ operation: 'add', blocker: { type: 'ipaddress', area: 'full', value: 'test-client' } });
|
||||
}
|
||||
},
|
||||
});
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('updates the list when data changes', async () => {
|
||||
let blockCallback;
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
blockCallback = callback;
|
||||
if (event === 'change') {
|
||||
callback({ operation: 'add', blocker: { type: 'client', area: 'full', value: 'another-client' } });
|
||||
}
|
||||
},
|
||||
});
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
checkErrorNotThrown(next);
|
||||
blockCallback({ operation: 'add', blocker: { type: 'client', area: 'full', value: 'test-client' } });
|
||||
attachBlocker(req, res, next);
|
||||
expect(next).to.have.been.calledTwice;
|
||||
const calledWith = next.getCall(1).args;
|
||||
expect(calledWith[0].message).to.equal(apiError('clientBlocked'));
|
||||
expect(calledWith[0] instanceof Forbidden).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,332 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import cronMiddleware from '../../../../website/server/middlewares/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
import * as cronLib from '../../../../website/server/libs/cron';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
describe('cron middleware', () => {
|
||||
let res; let
|
||||
req;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analyticsService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('calls next when user is not attached', done => {
|
||||
res.locals.user = null;
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', done => {
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('should clear todos older than 30 days for free users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.not.exist;
|
||||
resolve();
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.exist;
|
||||
return resolve();
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.not.exist;
|
||||
return resolve();
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next if user was not modified after cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('runs cron if previous cron was incomplete', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does damage for missing dailies', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
await Promise.all([todo.save(), user.save()]);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('applies quest progress', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
|
||||
const questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
|
||||
const party = new Group({
|
||||
type: 'party',
|
||||
name: generateUUID(),
|
||||
leader: user._id,
|
||||
});
|
||||
party.quest.members[user._id] = true;
|
||||
party.quest.key = questKey;
|
||||
await party.save();
|
||||
|
||||
user.party._id = party._id;
|
||||
await user.save();
|
||||
|
||||
party.startQuest(user);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('recovers from failed cron and does not error when user is already cronning', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
const updatedUser = user.toObject();
|
||||
updatedUser.matchedCount = 0;
|
||||
|
||||
sandbox.spy(cronLib, 'recoverCron');
|
||||
|
||||
sandbox.stub(User, 'updateOne')
|
||||
.withArgs({
|
||||
_id: user._id,
|
||||
$or: [
|
||||
{ _cronSignature: 'NOT_RUNNING' },
|
||||
{ _cronSignature: { $lt: sinon.match.number } },
|
||||
],
|
||||
})
|
||||
.returns({
|
||||
exec () {
|
||||
return Promise.resolve(updatedUser);
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(cronLib.recoverCron).to.be.calledOnce;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature less than an hour ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (!err) return reject(new Error('Cron should have failed.'));
|
||||
expect(err.message).to.be.equal(expectedErrMessage);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cron should not run more than once', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
sandbox.spy(cronLib, 'cron');
|
||||
|
||||
await Promise.all([new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}), new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}), new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}, 400);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(cronLib.cron).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
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.ip = '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 the ip is blocked', () => {
|
||||
req.ip = '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);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,13 @@
|
||||
import moment from 'moment';
|
||||
import requireAgain from 'require-again';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import { model as Blocker } from '../../../../website/server/models/blocker';
|
||||
import common from '../../../../website/common';
|
||||
|
||||
const pathToUserSchema = '../../../../website/server/models/user/schema';
|
||||
|
||||
describe('User Model', () => {
|
||||
describe('.toJSON()', () => {
|
||||
it('keeps user._tmp when calling .toJSON', () => {
|
||||
@@ -912,4 +916,73 @@ describe('User Model', () => {
|
||||
expect(user.toJSON().flags.newStuff).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validates email', () => {
|
||||
it('does not throw an error for a valid email', () => {
|
||||
const user = new User();
|
||||
user.auth.local.email = 'hello@example.com';
|
||||
const errors = user.validateSync();
|
||||
expect(errors.errors['auth.local.email']).to.not.exist;
|
||||
});
|
||||
|
||||
it('throws an error if email is not valid', () => {
|
||||
const user = new User();
|
||||
user.auth.local.email = 'invalid-email';
|
||||
const errors = user.validateSync();
|
||||
expect(errors.errors['auth.local.email'].message).to.equal(common.i18n.t('invalidEmail'));
|
||||
});
|
||||
|
||||
it('throws an error if email is using a restricted domain', () => {
|
||||
const user = new User();
|
||||
user.auth.local.email = 'scammer@habitica.com';
|
||||
const errors = user.validateSync();
|
||||
expect(errors.errors['auth.local.email'].message).to.equal(common.i18n.t('invalidEmailDomain', { domains: 'habitica.com, habitrpg.com' }));
|
||||
});
|
||||
|
||||
it('throws an error if email was blocked specifically', () => {
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@example.com' } });
|
||||
},
|
||||
});
|
||||
const schema = requireAgain(pathToUserSchema).UserSchema;
|
||||
const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com'));
|
||||
expect(valid).to.equal(false);
|
||||
});
|
||||
|
||||
it('throws an error if email domain was blocked', () => {
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: '@example.com' } });
|
||||
},
|
||||
});
|
||||
const schema = requireAgain(pathToUserSchema).UserSchema;
|
||||
const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com'));
|
||||
expect(valid).to.equal(false);
|
||||
});
|
||||
|
||||
it('throws an error if user portion of email was blocked', () => {
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@' } });
|
||||
},
|
||||
});
|
||||
const schema = requireAgain(pathToUserSchema).UserSchema;
|
||||
const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('blocked@example.com'));
|
||||
expect(valid).to.equal(false);
|
||||
});
|
||||
|
||||
it('does not throw an error if email is not blocked', () => {
|
||||
sandbox.stub(Blocker, 'watchBlockers').returns({
|
||||
on: (event, callback) => {
|
||||
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: '@example.com' } });
|
||||
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'blocked@' } });
|
||||
callback({ operation: 'add', blocker: { type: 'email', area: 'full', value: 'bad@test.com' } });
|
||||
},
|
||||
});
|
||||
const schema = requireAgain(pathToUserSchema).UserSchema;
|
||||
const valid = schema.paths['auth.local.email'].options.validate.every(v => v.validator('good@test.com'));
|
||||
expect(valid).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,34 +101,6 @@ describe('GET /tasks/user', () => {
|
||||
expect(allCompletedTodos[allCompletedTodos.length - 1].text).to.equal('todo to complete 2');
|
||||
});
|
||||
|
||||
it('returns only some completed todos if req.query.type is "completedTodos" or "_allCompletedTodos"', async () => {
|
||||
const LIMIT = 30;
|
||||
const numberOfTodos = LIMIT + 1;
|
||||
const todosInput = [];
|
||||
|
||||
for (let i = 0; i < numberOfTodos; i += 1) {
|
||||
todosInput[i] = { text: `todo to complete ${i}`, type: 'todo' };
|
||||
}
|
||||
const todos = await user.post('/tasks/user', todosInput);
|
||||
await user.sync();
|
||||
const initialTodoCount = user.tasksOrder.todos.length;
|
||||
|
||||
for (let i = 0; i < numberOfTodos; i += 1) {
|
||||
const id = todos[i]._id;
|
||||
|
||||
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
await user.sync();
|
||||
|
||||
expect(user.tasksOrder.todos.length).to.equal(initialTodoCount - numberOfTodos);
|
||||
|
||||
const completedTodos = await user.get('/tasks/user?type=completedTodos');
|
||||
expect(completedTodos.length).to.equal(LIMIT);
|
||||
|
||||
const allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos');
|
||||
expect(allCompletedTodos.length).to.equal(numberOfTodos);
|
||||
});
|
||||
|
||||
it('returns dailies with isDue for the date specified', async () => {
|
||||
// @TODO Add required format
|
||||
const startDate = moment().subtract('1', 'days').toISOString();
|
||||
|
||||
@@ -238,6 +238,28 @@ describe('POST /user/auth/reset-password-set-new-one', () => {
|
||||
expect(isPassValid).to.equal(true);
|
||||
});
|
||||
|
||||
it('changes the apiToken on password reset', async () => {
|
||||
const user = await generateUser();
|
||||
const previousToken = user.apiToken;
|
||||
|
||||
const code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({ days: 1 }),
|
||||
}));
|
||||
await user.updateOne({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
await api.post(`${endpoint}`, {
|
||||
newPassword: 'my new password',
|
||||
confirmPassword: 'my new password',
|
||||
code,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.apiToken).to.not.eql(previousToken);
|
||||
});
|
||||
|
||||
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
|
||||
@@ -27,11 +27,30 @@ describe('PUT /user/auth/update-password', async () => {
|
||||
newPassword,
|
||||
confirmPassword: newPassword,
|
||||
});
|
||||
expect(response).to.eql({});
|
||||
|
||||
expect(response).to.exist;
|
||||
expect(response.apiToken).to.exist;
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.hashed_password).to.not.eql(previousHashedPassword);
|
||||
});
|
||||
|
||||
it('should change the apiToken on password change', async () => {
|
||||
const previousToken = user.apiToken;
|
||||
const response = await user.put(ENDPOINT, {
|
||||
password,
|
||||
newPassword,
|
||||
confirmPassword: newPassword,
|
||||
});
|
||||
|
||||
const newToken = response.apiToken;
|
||||
expect(newToken).to.exist;
|
||||
|
||||
await user.sync();
|
||||
expect(user.apiToken).to.eql(newToken);
|
||||
expect(user.apiToken).to.not.eql(previousToken);
|
||||
});
|
||||
|
||||
it('returns an error when confirmPassword does not match newPassword', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
password,
|
||||
|
||||
@@ -133,21 +133,21 @@ describe('Content Schedule', () => {
|
||||
});
|
||||
|
||||
it('sets the end date for a gala', () => {
|
||||
const date = new Date('2024-05-20');
|
||||
const date = new Date('2024-05-31');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('sets the end date for a winter gala', () => {
|
||||
const date = new Date('2024-12-22');
|
||||
const date = new Date('2025-02-28');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('sets the end date in new year for a winter gala', () => {
|
||||
const date = new Date('2025-01-04');
|
||||
const date = new Date('2025-02-28');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('uses correct date for first hours of the month', () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('Shop Featured Items', () => {
|
||||
});
|
||||
|
||||
it('contains the current premium hatching potions', () => {
|
||||
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
|
||||
clock = Sinon.useFakeTimers(new Date('2024-04-09'));
|
||||
const items = featuredItems.market();
|
||||
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
|
||||
});
|
||||
|
||||
@@ -19,6 +19,6 @@ const sinonStubPromise = require('sinon-stub-promise');
|
||||
sinonStubPromise(global.sinon);
|
||||
global.sandbox = sinon.createSandbox();
|
||||
|
||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
||||
const setupNconf = require('../../website/server/libs/setupNconf').default;
|
||||
|
||||
setupNconf('./config.json.example');
|
||||
|
||||
@@ -74,15 +74,10 @@ export async function getDocument (collectionName, doc) {
|
||||
}
|
||||
|
||||
before(done => {
|
||||
mongoose.connection.on('open', err => {
|
||||
if (err) return done(err);
|
||||
return resetHabiticaDB()
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
mongoose.connection.once('open', async err => {
|
||||
if (err) throw err;
|
||||
await resetHabiticaDB();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const nconf = require('nconf');
|
||||
const mongoose = require('mongoose');
|
||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
||||
const setupNconf = require('../../website/server/libs/setupNconf').default;
|
||||
|
||||
// fix further imports of require/import syntaxes
|
||||
require('@babel/register');
|
||||
|
||||
@@ -3,6 +3,7 @@ module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
es2021: true,
|
||||
},
|
||||
extends: [
|
||||
'habitrpg/lib/vue',
|
||||
@@ -39,7 +40,4 @@ module.exports = {
|
||||
order: ['template', 'style', 'script'],
|
||||
}],
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/* eslint-disable import/no-commonjs */
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
],
|
||||
};
|
||||
@@ -12,6 +12,7 @@
|
||||
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
||||
<link rel="mask-icon" href="/static/icons/favicon.ico">
|
||||
<meta property="og:image" content="/static/emails/images/meta-image.png" />
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-screen">
|
||||
@@ -28,10 +29,9 @@
|
||||
</div>
|
||||
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
|
||||
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
|
||||
<!-- Translations -->
|
||||
<script type='text/javascript' src='/api/v4/i18n/browser-script'></script>
|
||||
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
|
||||
</body>
|
||||
</html>
|
||||
11833
website/client/package-lock.json
generated
11833
website/client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,28 +3,26 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
|
||||
"lint": "vue-cli-service lint .",
|
||||
"lint-no-fix": "vue-cli-service lint --no-fix .",
|
||||
"serve": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run",
|
||||
"test:unit:watch": "vitest watch",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path ../../.gitignore --fix .",
|
||||
"lint-no-fix": "eslint --ext .js,.vue --no-fix src",
|
||||
"postinstall": "node ./scripts/npm-postinstall.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-eslint": "^5.0.8",
|
||||
"@vue/cli-plugin-router": "^5.0.8",
|
||||
"@vue/cli-plugin-unit-mocha": "^5.0.8",
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@froxz/vite-plugin-s3": "^1.6.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.3",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.21.3",
|
||||
"assert": "^2.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^0.28.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"core-js": "^3.33.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-habitrpg": "6.2.0",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
@@ -34,31 +32,34 @@
|
||||
"intro.js": "^7.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"nconf": "^0.12.1",
|
||||
"sass": "^1.63.4",
|
||||
"sass-loader": "^14.1.1",
|
||||
"sinon": "^17.0.1",
|
||||
"stopword": "^2.0.8",
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.9.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-template-babel-compiler": "^2.0.0",
|
||||
"vue-template-compiler": "^2.7.10",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||
"@vitest/browser": "^3.0.5",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
"chai": "^5.1.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
</div>
|
||||
<snackbars />
|
||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||
<user-main v-else />
|
||||
<div v-else>
|
||||
<user-main />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#loading-screen-inapp {
|
||||
#melior {
|
||||
@@ -90,7 +92,7 @@
|
||||
</style>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.modal-backdrop {
|
||||
opacity: .9 !important;
|
||||
@@ -108,16 +110,16 @@ import axios from 'axios';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
import userMain from '@/pages/user-main';
|
||||
import snackbars from '@/components/snackbars/notifications';
|
||||
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL;
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
snackbars,
|
||||
userMain,
|
||||
userMain: () => import('@/pages/user-main'),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -221,11 +223,10 @@ export default {
|
||||
|
||||
const errorData = error.response.data;
|
||||
const errorMessage = errorData.message || errorData;
|
||||
const errorCode = errorData.error;
|
||||
|
||||
// Check for conditions to reset the user auth
|
||||
// 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) {
|
||||
// If 'invalid_credentials' signaled, force logout
|
||||
if (error.response.status === 401 && errorCode === 'invalid_credentials') {
|
||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||
return null;
|
||||
}
|
||||
@@ -268,16 +269,29 @@ export default {
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||
|
||||
if (this.isStaticPage || !this.isUserLoggedIn) {
|
||||
this.hideLoadingScreen();
|
||||
// Check if we need to show password change success message
|
||||
if (sessionStorage.getItem('passwordChangeSuccess') === 'true') {
|
||||
sessionStorage.removeItem('passwordChangeSuccess');
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
title: 'Habitica',
|
||||
text: this.$t('passwordSuccess'),
|
||||
type: 'success',
|
||||
timeout: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.$router.onReady(() => {
|
||||
if (this.isStaticPage || !this.isUserLoggedIn) {
|
||||
this.hideLoadingScreen();
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
hideLoadingScreen () {
|
||||
this.loading = false;
|
||||
},
|
||||
checkForBannedUser (error) {
|
||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
const errorMessage = error.response.data.message;
|
||||
|
||||
@@ -301,4 +315,3 @@ export default {
|
||||
</script>
|
||||
|
||||
<style src="@/assets/scss/index.scss" lang="scss"></style>
|
||||
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice {
|
||||
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice, .Mount_Head_Dragon-Hydra, .Mount_Body_Dragon-Hydra {
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
@@ -190,6 +190,14 @@
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Head_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_airship, .background_clocktower, .background_steamworks {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
|
||||
@@ -2001,6 +2001,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_sirens_lair {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_sirens_lair.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_slimy_swamp {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_slimy_swamp.png');
|
||||
width: 141px;
|
||||
@@ -2176,11 +2181,21 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_summer_seashore {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_summer_seashore.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_sunken_ship {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_sunken_ship.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_sunny_street_with_shops {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_sunny_street_with_shops.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_sunset_meadow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_sunset_meadow.png');
|
||||
width: 141px;
|
||||
@@ -2266,6 +2281,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_trail_through_a_forest {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_trail_through_a_forest.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_training_grounds {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_training_grounds.png');
|
||||
width: 141px;
|
||||
@@ -29540,6 +29560,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_beekeepersSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_beekeepersSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_blueMoonShozoku {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blueMoonShozoku.png');
|
||||
width: 114px;
|
||||
@@ -29670,6 +29695,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_flyFishingWaders {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_flyFishingWaders.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_funnyFoolCostume {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_funnyFoolCostume.png');
|
||||
width: 114px;
|
||||
@@ -29680,6 +29710,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_gildedKnightsPlate {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_gildedKnightsPlate.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_gladiatorArmor {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_gladiatorArmor.png');
|
||||
width: 90px;
|
||||
@@ -29870,6 +29905,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_redWaistcoat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_redWaistcoat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_robeOfDiamonds {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_robeOfDiamonds.png');
|
||||
width: 114px;
|
||||
@@ -30135,6 +30175,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_beekeepersHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_beekeepersHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_bigWig {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_bigWig.png');
|
||||
width: 90px;
|
||||
@@ -30275,6 +30320,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_flyFishingHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_flyFishingHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_frostedHelm {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_frostedHelm.png');
|
||||
width: 114px;
|
||||
@@ -30290,6 +30340,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_gildedKnightsHelm {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_gildedKnightsHelm.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_gladiatorHelm {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_gladiatorHelm.png');
|
||||
width: 90px;
|
||||
@@ -30490,6 +30545,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_redNewsieHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_redNewsieHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_regalCrown {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_regalCrown.png');
|
||||
width: 114px;
|
||||
@@ -30630,6 +30690,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_beekeepersHive {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_beekeepersHive.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_birthdayBanner {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_birthdayBanner.png');
|
||||
width: 114px;
|
||||
@@ -30730,6 +30795,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_flyFishingRod {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_flyFishingRod.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_gardenersSpade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenersSpade.png');
|
||||
width: 114px;
|
||||
@@ -31075,6 +31145,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_beekeepersSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_beekeepersSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_blueMoonShozoku {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blueMoonShozoku.png');
|
||||
width: 114px;
|
||||
@@ -31205,6 +31280,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_flyFishingWaders {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_flyFishingWaders.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_funnyFoolCostume {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_funnyFoolCostume.png');
|
||||
width: 114px;
|
||||
@@ -31215,6 +31295,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_gildedKnightsPlate {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_gildedKnightsPlate.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_gladiatorArmor {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_gladiatorArmor.png');
|
||||
width: 90px;
|
||||
@@ -31405,6 +31490,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_redWaistcoat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_redWaistcoat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_robeOfDiamonds {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_robeOfDiamonds.png');
|
||||
width: 114px;
|
||||
@@ -31640,6 +31730,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_beekeepersSmoker {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_beekeepersSmoker.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_blueKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_blueKite.png');
|
||||
width: 114px;
|
||||
@@ -31760,6 +31855,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_gildedKnightsSpear {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gildedKnightsSpear.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_glassblowersBlowpipe {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_glassblowersBlowpipe.png');
|
||||
width: 114px;
|
||||
@@ -35525,6 +35625,46 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202505 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202505.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.headAccessory_mystery_202505 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_mystery_202505.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202506 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202506.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202506 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202506.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202507 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202507.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.head_mystery_202507 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202507.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202508 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202508.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.weapon_mystery_202508 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202508.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.broad_armor_mystery_301404 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
|
||||
width: 90px;
|
||||
@@ -37070,6 +37210,26 @@
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.broad_armor_special_summer2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_summer2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2025Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.broad_armor_special_summer2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 108px;
|
||||
}
|
||||
.broad_armor_special_summer2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -37300,6 +37460,26 @@
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.head_special_summer2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_summer2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2025Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.head_special_summer2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 108px;
|
||||
}
|
||||
.head_special_summer2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -37475,6 +37655,21 @@
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.shield_special_summer2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_summer2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 108px;
|
||||
}
|
||||
.shield_special_summer2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -37695,6 +37890,26 @@
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.slim_armor_special_summer2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_summer2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2025Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.slim_armor_special_summer2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 108px;
|
||||
}
|
||||
.slim_armor_special_summer2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -37915,6 +38130,26 @@
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.weapon_special_summer2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_summer2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2025Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.weapon_special_summer2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 108px;
|
||||
}
|
||||
.weapon_special_summer2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -40813,6 +41048,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_BearCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_BearCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -41268,6 +41508,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Cactus-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Cactus-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -42018,6 +42263,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dragon-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dragon-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -42468,6 +42718,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_FlyingPig-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_FlyingPig-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -42768,6 +43023,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Fox-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Fox-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -43508,6 +43768,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_LionCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_LionCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -44078,6 +44343,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_PandaCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_PandaCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -44378,6 +44648,56 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-Base.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-CottonCandyBlue.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-CottonCandyPink.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-Desert.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-Golden.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-Red.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-Shade.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-Skeleton.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-White.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Platypus-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Platypus-Zombie.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Pterodactyl-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Pterodactyl-Base.png');
|
||||
width: 105px;
|
||||
@@ -45383,6 +45703,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_TigerCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_TigerCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -45993,6 +46318,11 @@
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
.Mount_Body_Wolf-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Opal.png');
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
.Mount_Body_Wolf-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Peppermint.png');
|
||||
width: 135px;
|
||||
@@ -46593,6 +46923,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_BearCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_BearCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -47048,6 +47383,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Cactus-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Cactus-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -47798,6 +48138,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dragon-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dragon-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -48248,6 +48593,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_FlyingPig-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_FlyingPig-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -48548,6 +48898,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Fox-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Fox-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -49288,6 +49643,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_LionCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_LionCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -49858,6 +50218,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_PandaCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_PandaCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -50158,6 +50523,56 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-Base.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-CottonCandyBlue.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-CottonCandyPink.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-Desert.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-Golden.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-Red.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-Shade.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-Skeleton.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-White.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Platypus-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Platypus-Zombie.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Pterodactyl-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Pterodactyl-Base.png');
|
||||
width: 105px;
|
||||
@@ -51163,6 +51578,11 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_TigerCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Opal.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_TigerCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Peppermint.png');
|
||||
width: 105px;
|
||||
@@ -51773,6 +52193,11 @@
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
.Mount_Head_Wolf-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Opal.png');
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
}
|
||||
.Mount_Head_Wolf-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Peppermint.png');
|
||||
width: 135px;
|
||||
@@ -52393,6 +52818,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Peppermint.png');
|
||||
width: 81px;
|
||||
@@ -52878,6 +53308,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Peppermint.png');
|
||||
width: 81px;
|
||||
@@ -53668,6 +54103,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Peppermint.png');
|
||||
width: 81px;
|
||||
@@ -54153,6 +54593,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Peppermint.png');
|
||||
width: 81px;
|
||||
@@ -54483,6 +54928,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Peppermint.png');
|
||||
width: 81px;
|
||||
@@ -55258,6 +55708,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Peppermint.png');
|
||||
width: 81px;
|
||||
@@ -55748,13 +56203,13 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Cryptid {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Cryptid.png');
|
||||
.Pet-PandaCub-Cupid {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Cupid.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Cupid {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Cupid.png');
|
||||
.Pet-PandaCub-Cyptid {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Cyptid.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
@@ -55858,6 +56313,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Peppermint.png');
|
||||
width: 81px;
|
||||
@@ -56173,6 +56633,56 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-Base.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-CottonCandyBlue.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-CottonCandyPink.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-Desert.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-Golden.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-Red.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-Shade.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-Skeleton.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-White.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Platypus-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Platypus-Zombie.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Pterodactyl-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Pterodactyl-Base.png');
|
||||
width: 81px;
|
||||
@@ -57198,6 +57708,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Peppermint.png');
|
||||
width: 81px;
|
||||
@@ -57838,6 +58353,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Opal {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Opal.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Peppermint {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Peppermint.png');
|
||||
width: 81px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.featured-label {
|
||||
width: auto;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
$grid-gutter-width: 24px;
|
||||
|
||||
// Bootstrap and its default variables
|
||||
@import 'node_modules/bootstrap/scss/bootstrap';
|
||||
@import '~/bootstrap/scss/bootstrap';
|
||||
|
||||
// Bootstrap Vue styles
|
||||
@import 'node_modules/bootstrap-vue/dist/bootstrap-vue';
|
||||
@import '~/bootstrap-vue/dist/bootstrap-vue';
|
||||
@@ -316,3 +316,9 @@
|
||||
line-height: 2;
|
||||
padding: 2px 2px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h1 {
|
||||
margin-top: 0px;
|
||||
|
||||
@@ -61,13 +61,13 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
|
||||
&.input-valid {
|
||||
padding-right: 27px;
|
||||
background-image: url(~@/assets/svg/for-css/check.svg);
|
||||
background-image: url(@/assets/svg/for-css/check.svg);
|
||||
background-size: 1rem;
|
||||
}
|
||||
|
||||
&.input-invalid {
|
||||
padding-right: 40px;
|
||||
background-image: url(~@/assets/svg/for-css/alert.svg);
|
||||
background-image: url(@/assets/svg/for-css/alert.svg);
|
||||
background-size: 16px 16px;
|
||||
border-color: $red-100 !important;
|
||||
}
|
||||
@@ -239,7 +239,7 @@ $bg-disabled-control: $gray-10;
|
||||
&:checked~.custom-control-label::after {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-image: url(~@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-size: 13px 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
}
|
||||
|
||||
.iconalert-success::before {
|
||||
background-image: url(~@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-size: 13px 10px;
|
||||
background-color: #1ca372;
|
||||
}
|
||||
|
||||
.iconalert-warning::before, .iconalert-error::before {
|
||||
background-image: url(~@/assets/svg/for-css/alert-white.svg);
|
||||
background-image: url(@/assets/svg/for-css/alert-white.svg);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.modal {
|
||||
z-index: 1350;
|
||||
|
||||
@@ -46,13 +46,11 @@
|
||||
|
||||
.background {
|
||||
background-repeat: repeat-x;
|
||||
|
||||
height:216px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -67,6 +65,13 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shop-message {
|
||||
position: relative;
|
||||
height: 76px;
|
||||
margin: 71px auto;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.npc {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.container-fluid.static-view {
|
||||
margin: 5em 2em 0 2em;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- @TODO i18n. How to setup the strings with the router-link inside?-->
|
||||
<img
|
||||
:class="retiredChatPage ? 'mt-5' : 'image-404'"
|
||||
src="~@/assets/images/404.png"
|
||||
src="@/assets/images/404.png"
|
||||
>
|
||||
<div v-if="retiredChatPage">
|
||||
<h1>
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h1, .static-wrapper h1 {
|
||||
color: $purple-200;
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.btn-primary:active {
|
||||
border: 2px solid $purple-400 !important;
|
||||
@@ -193,10 +193,10 @@
|
||||
import Avatar from '../avatar';
|
||||
import { mapState } from '@/libs/store';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import warriorIcon from '@/assets/svg/warrior.svg';
|
||||
import rogueIcon from '@/assets/svg/rogue.svg';
|
||||
import healerIcon from '@/assets/svg/healer.svg';
|
||||
import wizardIcon from '@/assets/svg/wizard.svg';
|
||||
import warriorIcon from '@/assets/svg/warrior.svg?raw';
|
||||
import rogueIcon from '@/assets/svg/rogue.svg?raw';
|
||||
import healerIcon from '@/assets/svg/healer.svg?raw';
|
||||
import wizardIcon from '@/assets/svg/wizard.svg?raw';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h2 {
|
||||
color: $purple-200;
|
||||
@@ -100,7 +100,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
import closeIcon from '@/assets/svg/close.svg?raw';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/mixins.scss';
|
||||
@import '@/assets/scss/mixins.scss';
|
||||
|
||||
#generic-achievement {
|
||||
@include centeredModal();
|
||||
@@ -61,7 +61,7 @@
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
@@ -98,7 +98,7 @@
|
||||
<script>
|
||||
import achievements from '@/../../common/script/content/achievements';
|
||||
import { mapState } from '@/libs/store';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import svgClose from '@/assets/svg/close.svg?raw';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -58,7 +58,7 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#level-up {
|
||||
.modal-content {
|
||||
@@ -157,8 +157,8 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
|
||||
import Avatar from '../avatar';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
import { mapState } from '@/libs/store';
|
||||
import starGroup from '@/assets/svg/star-group.svg';
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||
import starGroup from '@/assets/svg/star-group.svg?raw';
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg?raw';
|
||||
|
||||
const levelQuests = {
|
||||
15: 'atom1',
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</h2>
|
||||
<img
|
||||
class="onboarding-complete-banner d-block"
|
||||
src="~@/assets/images/onboarding-complete-banner@2x.png"
|
||||
src="@/assets/images/onboarding-complete-banner@2x.png"
|
||||
>
|
||||
<p
|
||||
v-once
|
||||
@@ -59,7 +59,7 @@
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h2 {
|
||||
color: $purple-200;
|
||||
@@ -100,7 +100,7 @@ button {
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import svgClose from '@/assets/svg/close.svg?raw';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
|
||||
@@ -97,9 +97,9 @@ import { mapState } from '@/libs/store';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
|
||||
export default {
|
||||
components: [
|
||||
components: {
|
||||
Sprite,
|
||||
],
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
maxHealth,
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#won-challenge {
|
||||
.modal-body {
|
||||
@@ -96,7 +96,7 @@
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.purple {
|
||||
color: $purple-300;
|
||||
@@ -146,9 +146,9 @@
|
||||
<script>
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
import closeIcon from '@/components/shared/closeIcon';
|
||||
import sparkles from '@/assets/svg/star-group.svg';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
import stars from '@/assets/svg/sparkles-left.svg';
|
||||
import sparkles from '@/assets/svg/star-group.svg?raw';
|
||||
import gem from '@/assets/svg/gem.svg?raw';
|
||||
import stars from '@/assets/svg/sparkles-left.svg?raw';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="row standard-page col-12 d-flex justify-content-center">
|
||||
<div class="admin-panel-content">
|
||||
<h1>Admin Panel</h1>
|
||||
<h1>{{ $t("adminPanel") }}</h1>
|
||||
<form
|
||||
class="form-inline"
|
||||
@submit.prevent="searchUsers(userIdentifier)"
|
||||
@@ -72,7 +72,7 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: 'Admin Panel',
|
||||
section: this.$t('adminPanel'),
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
@@ -55,7 +55,7 @@
|
||||
<script>
|
||||
import VueRouter from 'vue-router';
|
||||
import { mapState } from '@/libs/store';
|
||||
import LoadingSpinner from '../ui/loadingSpinner';
|
||||
import LoadingSpinner from '../../ui/loadingSpinner';
|
||||
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
|
||||
@@ -38,12 +38,17 @@
|
||||
>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
:id="permission.key"
|
||||
v-model="hero.permissions[permission.key]"
|
||||
:disabled="!hasPermission(user, permission.key)"
|
||||
:disabled="!hasPermission(user, permission.key)
|
||||
|| (hero.permissions.fullAccess && permission.key !== 'fullAccess')"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label class="custom-control-label">
|
||||
<label
|
||||
class="custom-control-label"
|
||||
:for="permission.key"
|
||||
>
|
||||
{{ permission.name }}<br>
|
||||
<small class="text-secondary">{{ permission.description }}</small>
|
||||
</label>
|
||||
@@ -124,7 +129,10 @@
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
<b
|
||||
v-if="hasUnsavedChanges"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
@@ -147,7 +155,7 @@ import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
const permissionList = [
|
||||
{
|
||||
@@ -175,6 +183,11 @@ const permissionList = [
|
||||
name: 'Challenge Admin',
|
||||
description: 'Can create official habitica challenges and admin all challenges',
|
||||
},
|
||||
{
|
||||
key: 'accessControl',
|
||||
name: 'Access Control',
|
||||
description: 'Can manage IP-Address, Client and E-Mail blockers',
|
||||
},
|
||||
{
|
||||
key: 'coupons',
|
||||
name: 'Coupon Creator',
|
||||
@@ -126,7 +126,7 @@
|
||||
@click="changeApiToken()"
|
||||
>
|
||||
Change API Token
|
||||
</a>
|
||||
</a>
|
||||
<div
|
||||
v-if="tokenModified"
|
||||
>
|
||||
@@ -46,7 +46,7 @@
|
||||
:
|
||||
<span :class="{ ownedItem: !item.neverOwned }">{{ item.text }}</span>
|
||||
</span>
|
||||
- {{ itemType }}.{{item.key}} - <i> {{ item.set }}</i>
|
||||
- {{ itemType }}.{{ item.key }} - <i> {{ item.set }}</i>
|
||||
|
||||
<div
|
||||
v-if="item.modified"
|
||||
@@ -16,9 +16,9 @@
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.flags, unModifiedHero.flags],
|
||||
[hero.auth, unModifiedHero.auth],
|
||||
[hero.balance, unModifiedHero.balance],
|
||||
[hero.secret, unModifiedHero.secret])"
|
||||
[hero.auth, unModifiedHero.auth],
|
||||
[hero.balance, unModifiedHero.balance],
|
||||
[hero.secret, unModifiedHero.secret])"
|
||||
/>
|
||||
|
||||
<subscription-and-perks
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<contributor-details
|
||||
:hero="hero"
|
||||
:hasUnsavedChanges="hasUnsavedChanges(
|
||||
:has-unsaved-changes="hasUnsavedChanges(
|
||||
[hero.contributor, unModifiedHero.contributor],
|
||||
[hero.permissions, unModifiedHero.permissions],
|
||||
[hero.secret, unModifiedHero.secret],
|
||||
@@ -149,7 +149,7 @@ import Achievements from './achievements.vue';
|
||||
import UserHistory from './userHistory.vue';
|
||||
import Stats from './stats.vue';
|
||||
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -32,38 +32,43 @@
|
||||
></p>
|
||||
</div>
|
||||
<div v-if="userHasParty">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Party ID
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData._id }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Estimated Member Count
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData.memberCount }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Leader
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
<span v-if="userIsPartyLeader">User is the party leader</span>
|
||||
<span v-else>Party leader is
|
||||
<router-link :to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}">
|
||||
{{ groupPartyData.leader }}
|
||||
</router-link>
|
||||
</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-danger"
|
||||
@click="removeFromParty()">Remove from Party</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Party ID
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData._id }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Estimated Member Count
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData.memberCount }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Leader
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
<span v-if="userIsPartyLeader">User is the party leader</span>
|
||||
<span v-else>Party leader is
|
||||
<router-link
|
||||
:to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}"
|
||||
>
|
||||
{{ groupPartyData.leader }}
|
||||
</router-link>
|
||||
</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-danger"
|
||||
@click="removeFromParty()"
|
||||
>
|
||||
Remove from Party
|
||||
</div>
|
||||
</div>
|
||||
<strong v-else>User is not in a party.</strong>
|
||||
<div class="subsection-start">
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({hero: {
|
||||
_id: hero._id,
|
||||
flags: hero.flags,
|
||||
balance: hero.balance,
|
||||
auth: hero.auth,
|
||||
secret: hero.secret,
|
||||
}, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<form
|
||||
@submit.prevent="saveHero({hero: {
|
||||
_id: hero._id,
|
||||
flags: hero.flags,
|
||||
balance: hero.balance,
|
||||
auth: hero.auth,
|
||||
secret: hero.secret,
|
||||
}, msg: 'Privileges or Gems or Moderation Notes'})"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -14,9 +16,12 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Privileges, Gem Balance
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -133,7 +138,10 @@
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
<b
|
||||
v-if="hasUnsavedChanges"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
@@ -19,7 +19,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.about-row {
|
||||
margin-left: 0px;
|
||||
@@ -8,9 +8,12 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Stats
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -18,47 +21,60 @@
|
||||
class="card-body"
|
||||
>
|
||||
<stats-row
|
||||
v-model="hero.stats.hp"
|
||||
label="Health"
|
||||
color="red-label"
|
||||
:max="maxHealth"
|
||||
v-model="hero.stats.hp" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.exp"
|
||||
label="Experience"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.exp" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.mp"
|
||||
label="Mana"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.mp" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.lvl"
|
||||
label="Level"
|
||||
step="1"
|
||||
min="0"
|
||||
:max="maxLevelHardCap"
|
||||
v-model="hero.stats.lvl" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.gp"
|
||||
label="Gold"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.gp" />
|
||||
/>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Selected Class</label>
|
||||
<div class="col-sm-9">
|
||||
<select
|
||||
id="selectedClass"
|
||||
v-model="hero.stats.class"
|
||||
class="form-control"
|
||||
:disabled="hero.stats.lvl < 10"
|
||||
>
|
||||
<option value="warrior">Warrior</option>
|
||||
<option value="wizard">Mage</option>
|
||||
<option value="healer">Healer</option>
|
||||
<option value="rogue">Rogue</option>
|
||||
</select>
|
||||
id="selectedClass"
|
||||
v-model="hero.stats.class"
|
||||
class="form-control"
|
||||
:disabled="hero.stats.lvl < 10"
|
||||
>
|
||||
<option value="warrior">
|
||||
Warrior
|
||||
</option>
|
||||
<option value="wizard">
|
||||
Mage
|
||||
</option>
|
||||
<option value="healer">
|
||||
Healer
|
||||
</option>
|
||||
<option value="rogue">
|
||||
Rogue
|
||||
</option>
|
||||
</select>
|
||||
<small>
|
||||
When changing class, players usually need stat points deallocated as well.
|
||||
</small>
|
||||
@@ -67,50 +83,59 @@
|
||||
|
||||
<h3>Stat Points</h3>
|
||||
<stats-row
|
||||
v-model="hero.stats.points"
|
||||
label="Unallocated"
|
||||
min="0"
|
||||
step="1"
|
||||
:max="maxStatPoints"
|
||||
v-model="hero.stats.points" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.str"
|
||||
label="Strength"
|
||||
color="red-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.str" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.int"
|
||||
label="Intelligence"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.int" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.per"
|
||||
label="Perception"
|
||||
color="purple-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.per" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.con"
|
||||
label="Constitution"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.con" />
|
||||
/>
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
@click="deallocateStatPoints">
|
||||
@click="deallocateStatPoints"
|
||||
>
|
||||
Deallocate all stat points
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" v-if="statPointsIncorrect">
|
||||
<div
|
||||
v-if="statPointsIncorrect"
|
||||
class="form-group row"
|
||||
>
|
||||
<div class="offset-sm-3 col-sm-9 text-danger">
|
||||
Error: Sum of stat points should equal the users level
|
||||
</div>
|
||||
@@ -118,35 +143,40 @@
|
||||
|
||||
<h3>Buffs</h3>
|
||||
<stats-row
|
||||
v-model="hero.stats.buffs.str"
|
||||
label="Strength"
|
||||
color="red-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.str" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.buffs.int"
|
||||
label="Intelligence"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.int" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.buffs.per"
|
||||
label="Perception"
|
||||
color="purple-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.per" />
|
||||
/>
|
||||
<stats-row
|
||||
v-model="hero.stats.buffs.con"
|
||||
label="Constitution"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.con" />
|
||||
/>
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
@click="resetBuffs">
|
||||
@click="resetBuffs"
|
||||
>
|
||||
Reset Buffs
|
||||
</button>
|
||||
</div>
|
||||
@@ -161,7 +191,10 @@
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
<b
|
||||
v-if="hasUnsavedChanges"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
@@ -170,7 +203,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.about-row {
|
||||
margin-left: 0px;
|
||||
@@ -189,7 +222,7 @@ import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
import StatsRow from './stats-row';
|
||||
|
||||
@@ -6,49 +6,71 @@
|
||||
}, msg: 'Subscription Perks' })"
|
||||
>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header"
|
||||
@click="expand = !expand">
|
||||
<div
|
||||
class="card-header"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{ 'open': expand }"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Payment method:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input v-model="hero.purchased.plan.paymentMethod"
|
||||
<input
|
||||
v-if="!isRegularPaymentMethod"
|
||||
v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-if="!isRegularPaymentMethod"
|
||||
>
|
||||
<select
|
||||
<select
|
||||
v-else
|
||||
v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="groupPlan">Group Plan</option>
|
||||
<option value="Stripe">Stripe</option>
|
||||
<option value="Apple">Apple</option>
|
||||
<option value="Google">Google</option>
|
||||
<option value="Amazon Payments">Amazon</option>
|
||||
<option value="PayPal">PayPal</option>
|
||||
<option value="Gift">Gift</option>
|
||||
<option value="">Clear out</option>
|
||||
</select>
|
||||
v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="groupPlan">
|
||||
Group Plan
|
||||
</option>
|
||||
<option value="Stripe">
|
||||
Stripe
|
||||
</option>
|
||||
<option value="Apple">
|
||||
Apple
|
||||
</option>
|
||||
<option value="Google">
|
||||
Google
|
||||
</option>
|
||||
<option value="Amazon Payments">
|
||||
Amazon
|
||||
</option>
|
||||
<option value="PayPal">
|
||||
PayPal
|
||||
</option>
|
||||
<option value="Gift">
|
||||
Gift
|
||||
</option>
|
||||
<option value="">
|
||||
Clear out
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -58,25 +80,40 @@
|
||||
Payment schedule:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input v-model="hero.purchased.plan.planId"
|
||||
<input
|
||||
v-if="!isRegularPlanId"
|
||||
v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-if="!isRegularPlanId"
|
||||
>
|
||||
<select
|
||||
<select
|
||||
v-else
|
||||
v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="basic_earned">Monthly recurring</option>
|
||||
<option value="basic_3mo">3 Months recurring</option>
|
||||
<option value="basic_6mo">6 Months recurring</option>
|
||||
<option value="basic_12mo">12 Months recurring</option>
|
||||
<option value="group_monthly">Group Plan (legacy)</option>
|
||||
<option value="group_plan_auto">Group Plan (auto)</option>
|
||||
<option value="">Clear out</option>
|
||||
</select>
|
||||
v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="basic_earned">
|
||||
Monthly recurring
|
||||
</option>
|
||||
<option value="basic_3mo">
|
||||
3 Months recurring
|
||||
</option>
|
||||
<option value="basic_6mo">
|
||||
6 Months recurring
|
||||
</option>
|
||||
<option value="basic_12mo">
|
||||
12 Months recurring
|
||||
</option>
|
||||
<option value="group_monthly">
|
||||
Group Plan (legacy)
|
||||
</option>
|
||||
<option value="group_plan_auto">
|
||||
Group Plan (auto)
|
||||
</option>
|
||||
<option value="">
|
||||
Clear out
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -86,43 +123,50 @@
|
||||
Customer ID:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.customerId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<input
|
||||
v-model="hero.purchased.plan.customerId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="hero.purchased.plan.planId === 'group_plan_auto'">
|
||||
<div
|
||||
v-if="hero.purchased.plan.planId === 'group_plan_auto'"
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Group Plan Memberships:
|
||||
</label>
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<loading-spinner
|
||||
v-if="!groupPlans"
|
||||
dark-color=true
|
||||
/>
|
||||
v-if="!groupPlans"
|
||||
dark-color="true"
|
||||
/>
|
||||
<b
|
||||
v-else-if="groupPlans.length === 0"
|
||||
class="text-danger col-form-label"
|
||||
v-else-if="groupPlans.length === 0"
|
||||
class="text-danger col-form-label"
|
||||
>User is not part of an active group plan!</b>
|
||||
<div
|
||||
v-else
|
||||
v-for="group in groupPlans"
|
||||
v-else
|
||||
:key="group._id"
|
||||
class="card mb-2">
|
||||
class="card mb-2"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ group.name }}
|
||||
<h6 class="card-title">
|
||||
{{ group.name }}
|
||||
<small class="float-right">{{ group._id }}</small>
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
<strong>Leader: </strong>
|
||||
<a
|
||||
v-if="group.leader !== hero._id"
|
||||
@click="switchUser(group.leader)"
|
||||
>{{ group.leader }}</a>
|
||||
<strong v-else class="text-success">This user</strong>
|
||||
<a
|
||||
v-if="group.leader !== hero._id"
|
||||
@click="switchUser(group.leader)"
|
||||
>{{ group.leader }}</a>
|
||||
<strong
|
||||
v-else
|
||||
class="text-success"
|
||||
>This user</strong>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<strong>Members: </strong> {{ group.memberCount }}
|
||||
@@ -190,16 +234,21 @@
|
||||
<strong class="input-group-text">
|
||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</strong>
|
||||
<a class="btn btn-danger"
|
||||
href="#"
|
||||
<a
|
||||
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId"
|
||||
v-b-modal.sub_termination_modal
|
||||
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId">
|
||||
class="btn btn-danger"
|
||||
href="#"
|
||||
>
|
||||
Terminate
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small v-if="!hero.purchased.plan.dateTerminated
|
||||
&& hero.purchased.plan.planId" class="text-success">
|
||||
<small
|
||||
v-if="!hero.purchased.plan.dateTerminated
|
||||
&& hero.purchased.plan.planId"
|
||||
class="text-success"
|
||||
>
|
||||
The subscription does not have a termination date and is active.
|
||||
</small>
|
||||
</div>
|
||||
@@ -235,11 +284,13 @@
|
||||
step="any"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<a class="btn btn-warning"
|
||||
<a
|
||||
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0"
|
||||
class="btn btn-warning"
|
||||
@click="applyExtraMonths"
|
||||
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0">
|
||||
>
|
||||
Apply Credit
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-secondary">
|
||||
@@ -339,19 +390,24 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'">
|
||||
<div
|
||||
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'"
|
||||
class="form-group row"
|
||||
>
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="beginGroupPlanConvert">
|
||||
@click="beginGroupPlanConvert"
|
||||
>
|
||||
Begin converting to group plan subscription
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="isConvertingToGroupPlan">
|
||||
<div
|
||||
v-if="isConvertingToGroupPlan"
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Group Plan group ID:
|
||||
</label>
|
||||
@@ -374,25 +430,40 @@
|
||||
class="btn btn-primary mt-1"
|
||||
@click="saveClicked"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
<b
|
||||
v-if="hasUnsavedChanges"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal id="sub_termination_modal" title="Set Termination Date">
|
||||
<b-modal
|
||||
id="sub_termination_modal"
|
||||
title="Set Termination Date"
|
||||
>
|
||||
<p>
|
||||
You can set the sub benefit termination date to today or to the last
|
||||
day of the current billing cycle. Any extra subscription credit will
|
||||
then be processed and automatically added onto the selected date.
|
||||
</p>
|
||||
<template #modal-footer>
|
||||
<div class="mt-3 btn btn-secondary" @click="$bvModal.hide('sub_termination_modal')">
|
||||
<div
|
||||
class="mt-3 btn btn-secondary"
|
||||
@click="$bvModal.hide('sub_termination_modal')"
|
||||
>
|
||||
Close
|
||||
</div>
|
||||
<div class="mt-3 btn btn-danger" @click="terminateSubscription()">
|
||||
<div
|
||||
class="mt-3 btn btn-danger"
|
||||
@click="terminateSubscription()"
|
||||
>
|
||||
Set to Today
|
||||
</div>
|
||||
<div class="mt-3 btn btn-danger" @click="terminateSubscription(todayWithRemainingCycle)">
|
||||
<div
|
||||
class="mt-3 btn btn-danger"
|
||||
@click="terminateSubscription(todayWithRemainingCycle)"
|
||||
>
|
||||
Set to {{ todayWithRemainingCycle.utc().format('MM/DD/YYYY') }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -401,7 +472,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.input-group-append {
|
||||
width: auto;
|
||||
@@ -420,15 +491,15 @@
|
||||
import isUUID from 'validator/es/lib/isUUID';
|
||||
import moment from 'moment';
|
||||
import { getPlanContext } from '@/../../common/script/cron';
|
||||
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import subscriptionBlocks from '../../../../../common/script/content/subscriptionBlocks';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
},
|
||||
mixins: [saveHero],
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
@@ -22,8 +22,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PurchaseHistoryTable from '../../ui/purchaseHistoryTable.vue';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import PurchaseHistoryTable from '../../../ui/purchaseHistoryTable.vue';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -150,7 +150,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.page-header.btn-flat {
|
||||
background: transparent;
|
||||
@@ -180,7 +180,7 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
@@ -13,9 +13,12 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
User Profile
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -66,7 +69,10 @@
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
<b
|
||||
v-if="hasUnsavedChanges"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
@@ -86,7 +92,7 @@ import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
function resetData (self) {
|
||||
self.expand = false;
|
||||
133
website/client/src/components/admin/blocker/blocker_form.vue
Normal file
133
website/client/src/components/admin/blocker/blocker_form.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div style="display: contents">
|
||||
<td>
|
||||
<select
|
||||
v-model="blocker.type"
|
||||
class="form-control"
|
||||
@change="onTypeChanged"
|
||||
>
|
||||
<option value="ipaddress">
|
||||
IP-Address
|
||||
</option>
|
||||
<option value="client">
|
||||
Client Identifier
|
||||
</option>
|
||||
<option value="email">
|
||||
E-Mail
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
v-model="blocker.area"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="full">
|
||||
Full
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model="blocker.value"
|
||||
class="form-control"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
:class="{ 'is-invalid input-invalid': !isValid }"
|
||||
@input="validateValue"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model="blocker.reason"
|
||||
class="form-control"
|
||||
>
|
||||
</td>
|
||||
<td
|
||||
colspan="3"
|
||||
class="text-right"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary mr-2"
|
||||
:disabled="!isValid"
|
||||
:class="{ disabled: !isValid }"
|
||||
@click="$emit('save', blocker)"
|
||||
>
|
||||
<span>Save</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="$emit('cancel')"
|
||||
>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
</td>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.btn-primary.disabled {
|
||||
background: #4F2A93;
|
||||
color: white;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import isIP from 'validator/es/lib/isIP';
|
||||
|
||||
export default {
|
||||
name: 'BlockerForm',
|
||||
props: {
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
blocker: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
type: '',
|
||||
area: '',
|
||||
value: '',
|
||||
reason: '',
|
||||
}),
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isValid: false,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.validateValue();
|
||||
},
|
||||
methods: {
|
||||
onTypeChanged () {
|
||||
if (this.blocker.type === 'email') {
|
||||
this.blocker.area = 'full';
|
||||
}
|
||||
this.validateValue();
|
||||
},
|
||||
validateValue () {
|
||||
if (this.blocker.type === 'ipaddress') {
|
||||
this.validateValueAsIpAddress();
|
||||
} else if (this.blocker.type === 'client') {
|
||||
this.validateValueAsClient();
|
||||
} else if (this.blocker.type === 'email') {
|
||||
this.validateValueAsEmail();
|
||||
}
|
||||
},
|
||||
validateValueAsEmail () {
|
||||
const emailRegex = /^([a-zA-Z0-9._%+-]*)@(?:[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})?$/;
|
||||
this.isValid = emailRegex.test(this.blocker.value) && this.blocker.value.length > 3;
|
||||
},
|
||||
validateValueAsIpAddress () {
|
||||
this.isValid = isIP(this.blocker.value);
|
||||
},
|
||||
validateValueAsClient () {
|
||||
this.isValid = this.blocker.value.length > 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
238
website/client/src/components/admin/blocker/index.vue
Normal file
238
website/client/src/components/admin/blocker/index.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="row standard-page col-12 d-flex justify-content-center">
|
||||
<div class="blocker-content">
|
||||
<h1>
|
||||
Blockers
|
||||
<button
|
||||
class="btn btn-primary float-right"
|
||||
@click="showCreateForm = true"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</h1>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Type <span
|
||||
id="type_tooltip"
|
||||
class="info-icon"
|
||||
>?</span>
|
||||
<b-tooltip
|
||||
target="type_tooltip"
|
||||
>
|
||||
<b>IP-Address</b> - Block access for a specific IP-Address
|
||||
<br>
|
||||
<br>
|
||||
<b>Client</b> - Block access for a client based on the "x-client" header.
|
||||
<br>
|
||||
<br>
|
||||
<b>E-Mail</b> - Blocks e-mails from being used for signup.
|
||||
</b-tooltip>
|
||||
</th>
|
||||
<th>
|
||||
Area <span
|
||||
id="area_tooltip"
|
||||
class="info-icon"
|
||||
>?</span>
|
||||
<b-tooltip
|
||||
target="area_tooltip"
|
||||
>
|
||||
<b>Full</b> - Block access to the entire site.
|
||||
<br>
|
||||
<br>
|
||||
<b>Payments</b> - Block access to any payment related functionality.
|
||||
</b-tooltip>
|
||||
</th>
|
||||
<th>Value</th>
|
||||
<th>Reason</th>
|
||||
<th>Source</th>
|
||||
<th>Created at</th>
|
||||
<th class="btncol"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="showCreateForm">
|
||||
<BlockerForm
|
||||
:is-new="true"
|
||||
:blocker="newBlocker"
|
||||
@save="createBlocker"
|
||||
@cancel="showCreateForm = false"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="blocker in blockers"
|
||||
:key="blocker._id"
|
||||
>
|
||||
<BlockerForm
|
||||
v-if="blocker._id === editedBlockerId"
|
||||
:blocker="blocker"
|
||||
@save="saveBlocker(blocker)"
|
||||
@cancel="editedBlockerId = null"
|
||||
/>
|
||||
<template v-else>
|
||||
<td>{{ getTypeName(blocker.type) }}</td>
|
||||
<td>{{ getAreaName(blocker.area) }}</td>
|
||||
<td>{{ blocker.value }}</td>
|
||||
<td>{{ blocker.reason || "--" }}</td>
|
||||
<td>{{ blocker.blockSource }}</td>
|
||||
<td>{{ blocker.createdAt }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-primary mr-2"
|
||||
@click="editBlocker(blocker._id)"
|
||||
>
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon icon-16"
|
||||
v-html="icons.editIcon"
|
||||
></span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="deleteBlocker(blocker._id)"
|
||||
>
|
||||
<span
|
||||
v-once
|
||||
class="svg-icon icon-16"
|
||||
v-html="icons.deleteIcon"
|
||||
></span>
|
||||
</button>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.blocker-content {
|
||||
flex: 0 0 100%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.4rem 0.75rem;
|
||||
}
|
||||
|
||||
.btncol {
|
||||
width: 123px;
|
||||
}
|
||||
|
||||
td {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 0.8rem;
|
||||
color: $purple-400;
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
background-color: $gray-500;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.info-icon:hover {
|
||||
background-color: $purple-400;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import editIcon from '@/assets/svg/edit.svg?raw';
|
||||
import deleteIcon from '@/assets/svg/delete.svg?raw';
|
||||
import BlockerForm from './blocker_form.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BlockerForm,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showCreateForm: false,
|
||||
newBlocker: {
|
||||
type: '',
|
||||
area: 'full',
|
||||
value: '',
|
||||
reason: '',
|
||||
},
|
||||
blockers: [],
|
||||
editedBlockerId: null,
|
||||
icons: Object.freeze({
|
||||
editIcon,
|
||||
deleteIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('siteBlockers'),
|
||||
});
|
||||
this.loadBlockers();
|
||||
},
|
||||
methods: {
|
||||
async loadBlockers () {
|
||||
this.blockers = await this.$store.dispatch('blockers:getBlockers');
|
||||
},
|
||||
editBlocker (id) {
|
||||
this.editedBlockerId = id;
|
||||
},
|
||||
async saveBlocker (blocker) {
|
||||
await this.$store.dispatch('blockers:updateBlocker', { blocker });
|
||||
this.editedBlockerId = null;
|
||||
this.loadBlockers();
|
||||
},
|
||||
async deleteBlocker (blockerId) {
|
||||
if (!window.confirm('Are you sure you want to delete this blocker?')) {
|
||||
return;
|
||||
}
|
||||
await this.$store.dispatch('blockers:deleteBlocker', { blockerId });
|
||||
this.loadBlockers();
|
||||
},
|
||||
async createBlocker (blocker) {
|
||||
await this.$store.dispatch('blockers:createBlocker', { blocker });
|
||||
this.showCreateForm = false;
|
||||
this.newBlocker = {
|
||||
type: '',
|
||||
area: 'full',
|
||||
value: '',
|
||||
reason: '',
|
||||
};
|
||||
this.loadBlockers();
|
||||
},
|
||||
|
||||
getTypeName (type) {
|
||||
switch (type) {
|
||||
case 'ipaddress':
|
||||
return 'IP-Address';
|
||||
case 'email':
|
||||
return 'E-Mail';
|
||||
case 'client':
|
||||
return 'Client Identifier';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
},
|
||||
getAreaName (area) {
|
||||
switch (area) {
|
||||
case 'full':
|
||||
return 'Full';
|
||||
case 'payments':
|
||||
return 'Payments';
|
||||
default:
|
||||
return area;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
40
website/client/src/components/admin/container.vue
Normal file
40
website/client/src/components/admin/container.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<secondary-menu class="col-12">
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'userSupport')"
|
||||
class="nav-link"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
{{ $t('adminPanel') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'accessControl')"
|
||||
class="nav-link"
|
||||
:to="{name: 'blockers'}"
|
||||
>
|
||||
{{ $t('siteBlockers') }}
|
||||
</router-link>
|
||||
</secondary-menu><div class="col-12">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import SecondaryMenu from '@/components/secondaryMenu';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecondaryMenu,
|
||||
},
|
||||
mixins: [
|
||||
userStateMixin,
|
||||
],
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -276,9 +276,9 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="time-travel"
|
||||
v-if="TIME_TRAVEL_ENABLED && user?.permissions?.fullAccess"
|
||||
:key="lastTimeJump"
|
||||
class="time-travel"
|
||||
>
|
||||
<a
|
||||
class="btn btn-secondary mr-1"
|
||||
@@ -299,7 +299,7 @@
|
||||
@click="resetTime()"
|
||||
>
|
||||
Reset
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
class="btn btn-secondary mr-1"
|
||||
@@ -403,7 +403,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
.footer-row {
|
||||
margin: 0;
|
||||
flex: 0 1 auto;
|
||||
@@ -838,12 +838,12 @@ import moment from 'moment';
|
||||
import Vue from 'vue';
|
||||
|
||||
// images
|
||||
import melior from '@/assets/svg/melior.svg';
|
||||
import bluesky from '@/assets/svg/bluesky.svg';
|
||||
import facebook from '@/assets/svg/facebook.svg';
|
||||
import instagram from '@/assets/svg/instagram.svg';
|
||||
import tumblr from '@/assets/svg/tumblr.svg';
|
||||
import heart from '@/assets/svg/heart.svg';
|
||||
import melior from '@/assets/svg/melior.svg?raw';
|
||||
import bluesky from '@/assets/svg/bluesky.svg?raw';
|
||||
import facebook from '@/assets/svg/facebook.svg?raw';
|
||||
import instagram from '@/assets/svg/instagram.svg?raw';
|
||||
import tumblr from '@/assets/svg/tumblr.svg?raw';
|
||||
import heart from '@/assets/svg/heart.svg?raw';
|
||||
|
||||
// components & modals
|
||||
import { mapState } from '@/libs/store';
|
||||
@@ -851,12 +851,14 @@ import buyGemsModal from './payments/buyGemsModal.vue';
|
||||
import reportBug from '@/mixins/reportBug.js';
|
||||
import { worldStateMixin } from '@/mixins/worldState';
|
||||
|
||||
const DEBUG_ENABLED = process.env.DEBUG_ENABLED === 'true'; // eslint-disable-line no-process-env
|
||||
const TIME_TRAVEL_ENABLED = process.env.TIME_TRAVEL_ENABLED === 'true'; // eslint-disable-line no-process-env
|
||||
const DEBUG_ENABLED = import.meta.env.DEBUG_ENABLED === 'true';
|
||||
const TIME_TRAVEL_ENABLED = import.meta.env.TIME_TRAVEL_ENABLED === 'true';
|
||||
|
||||
let sinon;
|
||||
if (TIME_TRAVEL_ENABLED) {
|
||||
// eslint-disable-next-line global-require
|
||||
sinon = await import('sinon');
|
||||
if (import.meta.env.TIME_TRAVEL_ENABLED === 'true') {
|
||||
(async () => {
|
||||
sinon = await import('sinon');
|
||||
})();
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -944,24 +946,28 @@ export default {
|
||||
},
|
||||
async jumpTime (amount) {
|
||||
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
|
||||
if (amount > 0) {
|
||||
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
|
||||
}
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
setTimeout(() => {
|
||||
if (amount > 0) {
|
||||
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
|
||||
}
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
}, 1000);
|
||||
},
|
||||
async resetTime () {
|
||||
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
|
||||
const time = new Date(response.data.data.time);
|
||||
Vue.config.clock.restore();
|
||||
Vue.config.clock = sinon.useFakeTimers({
|
||||
now: time,
|
||||
shouldAdvanceTime: true,
|
||||
});
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
setTimeout(() => {
|
||||
Vue.config.clock.restore();
|
||||
Vue.config.clock = sinon.useFakeTimers({
|
||||
now: time,
|
||||
shouldAdvanceTime: true,
|
||||
});
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
}, 1000);
|
||||
},
|
||||
addExp () {
|
||||
// @TODO: Name these variables better
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.form {
|
||||
margin: 0 auto;
|
||||
@@ -227,8 +227,8 @@ import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg?raw';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg?raw';
|
||||
|
||||
export default {
|
||||
name: 'AuthForm',
|
||||
@@ -290,7 +290,7 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -220,7 +220,6 @@
|
||||
v-if="forgotPassword"
|
||||
id="forgot-form"
|
||||
@submit.prevent="handleSubmit"
|
||||
@keyup.enter="handleSubmit"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div>
|
||||
@@ -268,12 +267,11 @@
|
||||
v-if="resetPasswordSetNewOne"
|
||||
id="reset-password-set-new-one-form"
|
||||
@submit.prevent="handleSubmit"
|
||||
@keyup.enter="handleSubmit"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div>
|
||||
<div
|
||||
class="svg-icon habitica-logo color"
|
||||
class="svg-icon habitica-logo"
|
||||
v-html="icons.habiticaIcon"
|
||||
></div>
|
||||
</div>
|
||||
@@ -355,7 +353,7 @@
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
@media only screen and (min-height: 1080px) {
|
||||
.bottom-wrap-register {
|
||||
@@ -491,7 +489,7 @@
|
||||
|
||||
#top-background {
|
||||
.seamless_stars_varied_opacity_repeat {
|
||||
background-image: url('~@/assets/images/auth/seamless_stars_varied_opacity.png');
|
||||
background-image: url('@/assets/images/auth/seamless_stars_varied_opacity.png');
|
||||
background-repeat: repeat-x;
|
||||
position: absolute;
|
||||
height: 500px;
|
||||
@@ -510,7 +508,7 @@
|
||||
position: relative;
|
||||
|
||||
.seamless_mountains_demo_repeat {
|
||||
background-image: url('~@/assets/images/auth/seamless_mountains_demo.png');
|
||||
background-image: url('@/assets/images/auth/seamless_mountains_demo.png');
|
||||
background-repeat: repeat-x;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
@@ -520,7 +518,7 @@
|
||||
}
|
||||
|
||||
.midground_foreground_extended2 {
|
||||
background-image: url('~@/assets/images/auth/midground_foreground_extended2.png');
|
||||
background-image: url('@/assets/images/auth/midground_foreground_extended2.png');
|
||||
position: relative;
|
||||
width: 1500px;
|
||||
max-width: 100%;
|
||||
@@ -611,11 +609,11 @@ import isEmail from 'validator/es/lib/isEmail';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
import exclamation from '@/assets/svg/exclamation.svg';
|
||||
import gryphon from '@/assets/svg/gryphon.svg';
|
||||
import habiticaIcon from '@/assets/svg/logo-horizontal.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
import exclamation from '@/assets/svg/exclamation.svg?raw';
|
||||
import gryphon from '@/assets/svg/gryphon.svg?raw';
|
||||
import habiticaIcon from '@/assets/svg/logo-horizontal.svg?raw';
|
||||
import googleIcon from '@/assets/svg/google.svg?raw';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg?raw';
|
||||
|
||||
export default {
|
||||
mixins: [sanitizeRedirect],
|
||||
@@ -726,9 +724,13 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
|
||||
|
||||
if (this.forgotPassword) {
|
||||
if (this.$route.query.email) {
|
||||
this.username = this.$route.query.email;
|
||||
}
|
||||
}
|
||||
hello.init({
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.avatar {
|
||||
width: 141px;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.bottom-banner {
|
||||
background: linear-gradient(114.26deg, $purple-300 0%, $purple-200 100%);
|
||||
@@ -55,7 +55,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg?raw';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
>
|
||||
<div
|
||||
v-for="option in items"
|
||||
:key="option.key"
|
||||
:id="option.imageName"
|
||||
:key="option.key"
|
||||
class="outer-option-background"
|
||||
:class="{
|
||||
premium: Boolean(option.gem),
|
||||
@@ -28,23 +28,22 @@
|
||||
v-if="!option.none"
|
||||
class="sprite"
|
||||
:prefix="option.isGear ? 'shop' : 'icon'"
|
||||
:imageName="option.imageName"
|
||||
:image-name="option.imageName"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="redline-outer"
|
||||
>
|
||||
<div class="redline"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="redline-outer"
|
||||
>
|
||||
<div class="redline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
import gold from '@/assets/svg/gold.svg';
|
||||
import gem from '@/assets/svg/gem.svg?raw';
|
||||
import gold from '@/assets/svg/gold.svg?raw';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
@@ -73,7 +72,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.customize-options {
|
||||
width: 100%;
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.sub-menu {
|
||||
display: flex;
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
|
||||
<script>
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
@@ -39,7 +40,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
bannedMessage () {
|
||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
const userId = parseSettings ? parseSettings.auth.apiId : '';
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h2 {
|
||||
color: $white;
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h2 {
|
||||
color: $white;
|
||||
@@ -134,7 +134,7 @@ label {
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/components/shared/closeIcon';
|
||||
import checkCircleIcon from '@/assets/svg/check_circle.svg';
|
||||
import checkCircleIcon from '@/assets/svg/check_circle.svg?raw';
|
||||
import { MODALS } from '@/libs/consts';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
@@ -380,9 +380,9 @@ import sidebarSection from '../sidebarSection';
|
||||
import userLink from '../userLink';
|
||||
import groupLink from '../groupLink';
|
||||
|
||||
import gemIcon from '@/assets/svg/gem.svg';
|
||||
import memberIcon from '@/assets/svg/member-icon.svg';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg';
|
||||
import gemIcon from '@/assets/svg/gem.svg?raw';
|
||||
import memberIcon from '@/assets/svg/member-icon.svg?raw';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg?raw';
|
||||
|
||||
const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge'];
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
// Have to use this, because v-markdown creates p element in h3. Scoping doesn't work with it.
|
||||
.challenge-title > p {
|
||||
display: -webkit-box;
|
||||
@@ -127,7 +127,7 @@
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.challenge {
|
||||
background-color: $white;
|
||||
@@ -377,14 +377,14 @@ import categoryTags from '../categories/categoryTags';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import gemIcon from '@/assets/svg/gem.svg';
|
||||
import memberIcon from '@/assets/svg/member-icon.svg';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg';
|
||||
import habitIcon from '@/assets/svg/habit.svg';
|
||||
import todoIcon from '@/assets/svg/todo.svg';
|
||||
import dailyIcon from '@/assets/svg/daily.svg';
|
||||
import rewardIcon from '@/assets/svg/reward.svg';
|
||||
import officialIcon from '@/assets/svg/official.svg';
|
||||
import gemIcon from '@/assets/svg/gem.svg?raw';
|
||||
import memberIcon from '@/assets/svg/member-icon.svg?raw';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg?raw';
|
||||
import habitIcon from '@/assets/svg/habit.svg?raw';
|
||||
import todoIcon from '@/assets/svg/todo.svg?raw';
|
||||
import dailyIcon from '@/assets/svg/daily.svg?raw';
|
||||
import rewardIcon from '@/assets/svg/reward.svg?raw';
|
||||
import officialIcon from '@/assets/svg/official.svg?raw';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#challenge-modal {
|
||||
h5 {
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#close-challenge-modal {
|
||||
h2 {
|
||||
@@ -98,7 +98,7 @@
|
||||
}
|
||||
|
||||
.support-habitica {
|
||||
background-image: url('~@/assets/svg/for-css/support-habitica-gems.svg');
|
||||
background-image: url('@/assets/svg/for-css/support-habitica-gems.svg?raw');
|
||||
width: 325px;
|
||||
height: 89px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.header-row {
|
||||
@@ -122,7 +122,7 @@ import challengeModal from './challengeModal';
|
||||
import externalLinks from '@/mixins/externalLinks';
|
||||
import challengeUtilities from '@/mixins/challengeUtilities';
|
||||
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import positiveIcon from '@/assets/svg/positive.svg?raw';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.no-challenge-section {
|
||||
padding: 2em;
|
||||
@@ -84,7 +84,7 @@ import markdownDirective from '@/directives/markdown';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
|
||||
import challengeItem from './challengeItem';
|
||||
import challengeIcon from '@/assets/svg/challenge.svg';
|
||||
import challengeIcon from '@/assets/svg/challenge.svg?raw';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.header-row {
|
||||
@@ -150,8 +150,8 @@ import challengeModal from './challengeModal';
|
||||
import challengeUtilities from '@/mixins/challengeUtilities';
|
||||
import externalLinks from '@/mixins/externalLinks';
|
||||
|
||||
import challengeIcon from '@/assets/svg/challenge.svg';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import challengeIcon from '@/assets/svg/challenge.svg?raw';
|
||||
import positiveIcon from '@/assets/svg/positive.svg?raw';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.modal-body {
|
||||
padding: 0px 8px 0px 8px;
|
||||
@@ -207,8 +207,8 @@ import { mapState } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import svgReport from '@/assets/svg/report.svg';
|
||||
import svgClose from '@/assets/svg/close.svg?raw';
|
||||
import svgReport from '@/assets/svg/report.svg?raw';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/tiers.scss';
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/tiers.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.autocomplete-results {
|
||||
padding: .5em;
|
||||
@@ -74,16 +74,16 @@
|
||||
<script>
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import tier1 from '@/assets/svg/tier-1.svg';
|
||||
import tier2 from '@/assets/svg/tier-2.svg';
|
||||
import tier3 from '@/assets/svg/tier-3.svg';
|
||||
import tier4 from '@/assets/svg/tier-4.svg';
|
||||
import tier5 from '@/assets/svg/tier-5.svg';
|
||||
import tier6 from '@/assets/svg/tier-6.svg';
|
||||
import tier7 from '@/assets/svg/tier-7.svg';
|
||||
import tier8 from '@/assets/svg/tier-mod.svg';
|
||||
import tier9 from '@/assets/svg/tier-staff.svg';
|
||||
import tierNPC from '@/assets/svg/tier-npc.svg';
|
||||
import tier1 from '@/assets/svg/tier-1.svg?raw';
|
||||
import tier2 from '@/assets/svg/tier-2.svg?raw';
|
||||
import tier3 from '@/assets/svg/tier-3.svg?raw';
|
||||
import tier4 from '@/assets/svg/tier-4.svg?raw';
|
||||
import tier5 from '@/assets/svg/tier-5.svg?raw';
|
||||
import tier6 from '@/assets/svg/tier-6.svg?raw';
|
||||
import tier7 from '@/assets/svg/tier-7.svg?raw';
|
||||
import tier8 from '@/assets/svg/tier-mod.svg?raw';
|
||||
import tier9 from '@/assets/svg/tier-staff.svg?raw';
|
||||
import tierNPC from '@/assets/svg/tier-npc.svg?raw';
|
||||
|
||||
export default {
|
||||
mixins: [styleHelper],
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.avatar {
|
||||
width: 10%;
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.modal-body {
|
||||
padding: 0px 8px 0px 8px;
|
||||
@@ -199,7 +199,7 @@
|
||||
import notifications from '@/mixins/notifications';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import svgClose from '@/assets/svg/close.svg?raw';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
|
||||
@@ -572,7 +572,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
$dialogMarginTop: 56px;
|
||||
$userCreationBgHeight: 105px;
|
||||
@@ -671,7 +671,7 @@
|
||||
}
|
||||
|
||||
.user-creation-bg {
|
||||
background-image: url('~@/assets/creator/creator-hills-bg.png');
|
||||
background-image: url('@/assets/creator/creator-hills-bg.png');
|
||||
height: $userCreationBgHeight;
|
||||
width: 219px;
|
||||
margin: 0 auto;
|
||||
@@ -1001,18 +1001,18 @@ import hairSettings from './avatarModal/hair-settings';
|
||||
import extraSettings from './avatarModal/extra-settings';
|
||||
import closeX from './ui/closeX';
|
||||
|
||||
import logoPurple from '@/assets/svg/logo-purple.svg';
|
||||
import bodyIcon from '@/assets/svg/body.svg';
|
||||
import accessoriesIcon from '@/assets/svg/accessories.svg';
|
||||
import skinIcon from '@/assets/svg/skin.svg';
|
||||
import hairIcon from '@/assets/svg/hair.svg';
|
||||
import backgroundsIcon from '@/assets/svg/backgrounds.svg';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
import hourglass from '@/assets/svg/hourglass.svg';
|
||||
import gold from '@/assets/svg/gold.svg';
|
||||
import arrowRight from '@/assets/svg/arrow_right.svg';
|
||||
import arrowLeft from '@/assets/svg/arrow_left.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import logoPurple from '@/assets/svg/logo-purple.svg?raw';
|
||||
import bodyIcon from '@/assets/svg/body.svg?raw';
|
||||
import accessoriesIcon from '@/assets/svg/accessories.svg?raw';
|
||||
import skinIcon from '@/assets/svg/skin.svg?raw';
|
||||
import hairIcon from '@/assets/svg/hair.svg?raw';
|
||||
import backgroundsIcon from '@/assets/svg/backgrounds.svg?raw';
|
||||
import gem from '@/assets/svg/gem.svg?raw';
|
||||
import hourglass from '@/assets/svg/hourglass.svg?raw';
|
||||
import gold from '@/assets/svg/gold.svg?raw';
|
||||
import arrowRight from '@/assets/svg/arrow_right.svg?raw';
|
||||
import arrowLeft from '@/assets/svg/arrow_left.svg?raw';
|
||||
import svgClose from '@/assets/svg/close.svg?raw';
|
||||
import { avatarEditorUtilities } from '../mixins/avatarEditUtilities';
|
||||
import Sprite from './ui/sprite';
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#external-link-modal {
|
||||
&.modal {
|
||||
@@ -174,8 +174,8 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import exclamationIcon from '@/assets/svg/exclamation.svg';
|
||||
import closeIcon from '@/assets/svg/new-close.svg';
|
||||
import exclamationIcon from '@/assets/svg/exclamation.svg?raw';
|
||||
import closeIcon from '@/assets/svg/new-close.svg?raw';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.face-avatar {
|
||||
width: 36px;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user