diff --git a/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js b/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js
index 107f729a7d..c83e4cbaf1 100644
--- a/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js
+++ b/test/api/v3/integration/hall/GET-hall_heroes_heroId.test.js
@@ -9,7 +9,7 @@ describe('GET /heroes/:heroId', () => {
const heroFields = [
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
- 'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret',
+ 'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
];
before(async () => {
diff --git a/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js b/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js
index 2f98e89b3f..2c27536a61 100644
--- a/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js
+++ b/test/api/v3/integration/hall/PUT-hall_heores_heroId.test.js
@@ -10,7 +10,7 @@ describe('PUT /heroes/:heroId', () => {
const heroFields = [
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
- 'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions',
+ 'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
];
before(async () => {
@@ -251,4 +251,159 @@ describe('PUT /heroes/:heroId', () => {
expect(updatedHero.apiToken).to.not.equal(originalToken);
expect(updatedHero.apiTokenObscured).to.not.exist;
});
+
+ it('updates purchased hair customization', async () => {
+ const hero = await generateUser();
+ const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
+ purchasedPath: 'purchased.hair.bangs.1',
+ purchasedVal: true,
+ });
+
+ // test response
+ expect(heroRes).to.have.all.keys(heroFields);
+ expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
+ expect(heroRes.profile).to.have.all.keys(['name']);
+
+ // test response values
+ expect(heroRes.purchased.hair.bangs['1']).to.equal(true);
+ // test hero values
+ await hero.sync();
+ expect(hero.purchased.hair.bangs['1']).to.equal(true);
+ });
+
+ it('updates purchased customization', async () => {
+ const hero = await generateUser();
+ const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
+ purchasedPath: 'purchased.background.beach',
+ purchasedVal: true,
+ });
+
+ // test response
+ expect(heroRes).to.have.all.keys(heroFields);
+ expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
+ expect(heroRes.profile).to.have.all.keys(['name']);
+
+ // test response values
+ expect(heroRes.purchased.background.beach).to.equal(true);
+ // test hero values
+ await hero.sync();
+ expect(hero.purchased.background.beach).to.equal(true);
+ });
+
+ it('updates giving nested achievement', async () => {
+ const hero = await generateUser();
+ const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
+ achievementPath: 'achievements.quests.dilatory',
+ achievementVal: 2,
+ });
+
+ // test response
+ expect(heroRes).to.have.all.keys(heroFields);
+ expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
+ expect(heroRes.profile).to.have.all.keys(['name']);
+
+ // test response values
+ expect(heroRes.achievements.quests.dilatory).to.equal(2);
+ // test hero values
+ await hero.sync();
+ expect(hero.achievements.quests.dilatory).to.equal(2);
+ });
+
+ it('updates taking away nested achievement', async () => {
+ const hero = await generateUser({ 'achievements.quests.dilatory': 3 });
+ expect(hero.achievements.quests.dilatory).to.equal(3);
+ const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
+ achievementPath: 'achievements.quests.dilatory',
+ achievementVal: 0,
+ });
+
+ // test response
+ expect(heroRes).to.have.all.keys(heroFields);
+ expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
+ expect(heroRes.profile).to.have.all.keys(['name']);
+
+ // test response values
+ expect(heroRes.achievements.quests.dilatory).to.equal(0);
+ // test hero values
+ await hero.sync();
+ expect(hero.achievements.quests.dilatory).to.equal(0);
+ });
+
+ it('updates giving achievement', async () => {
+ const hero = await generateUser();
+ const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
+ achievementPath: 'achievements.partyOn',
+ achievementVal: true,
+ });
+
+ // test response
+ expect(heroRes).to.have.all.keys(heroFields);
+ expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
+ expect(heroRes.profile).to.have.all.keys(['name']);
+
+ // test response values
+ expect(heroRes.achievements.partyOn).to.equal(true);
+ // test hero values
+ await hero.sync();
+ expect(hero.achievements.partyOn).to.equal(true);
+ });
+
+ it('updates taking away achievement', async () => {
+ const hero = await generateUser({ 'achievements.partyUp': true });
+ expect(hero.achievements.partyUp).to.equal(true);
+ const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
+ achievementPath: 'achievements.partyUp',
+ achievementVal: false,
+ });
+
+ // test response
+ expect(heroRes).to.have.all.keys(heroFields);
+ expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
+ expect(heroRes.profile).to.have.all.keys(['name']);
+
+ // test response values
+ expect(heroRes.achievements.partyUp).to.equal(false);
+ // test hero values
+ await hero.sync();
+ expect(hero.achievements.partyUp).to.equal(false);
+ });
+
+ it('updates giving numbered achievement', async () => {
+ const hero = await generateUser();
+ const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
+ achievementPath: 'achievements.streak',
+ achievementVal: 42,
+ });
+
+ // test response
+ expect(heroRes).to.have.all.keys(heroFields);
+ expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
+ expect(heroRes.profile).to.have.all.keys(['name']);
+
+ // test response values
+ expect(heroRes.achievements.streak).to.equal(42);
+ // test hero values
+ await hero.sync();
+ expect(hero.achievements.streak).to.equal(42);
+ });
+
+ it('updates setting numbered achievement to 0', async () => {
+ const hero = await generateUser({ 'achievements.streak': 42 });
+ expect(hero.achievements.streak).to.equal(42);
+ const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
+ achievementPath: 'achievements.streak',
+ achievementVal: 0,
+ });
+
+ // test response
+ expect(heroRes).to.have.all.keys(heroFields);
+ expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
+ expect(heroRes.profile).to.have.all.keys(['name']);
+
+ // test response values
+ expect(heroRes.achievements.streak).to.equal(0);
+ // test hero values
+ await hero.sync();
+ expect(hero.achievements.streak).to.equal(0);
+ });
});
diff --git a/website/client/src/components/admin-panel/user-support/achievements.vue b/website/client/src/components/admin-panel/user-support/achievements.vue
new file mode 100644
index 0000000000..d3ab101b34
--- /dev/null
+++ b/website/client/src/components/admin-panel/user-support/achievements.vue
@@ -0,0 +1,270 @@
+
+
+
+ Achievements
+
+
+
+
+
+
+ {{ achievementType }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/client/src/components/admin-panel/user-support/customizationsOwned.vue b/website/client/src/components/admin-panel/user-support/customizationsOwned.vue
new file mode 100644
index 0000000000..9b244efe60
--- /dev/null
+++ b/website/client/src/components/admin-panel/user-support/customizationsOwned.vue
@@ -0,0 +1,245 @@
+
+
+
+ Customizations
+
+
+
+
+
+ {{ itemType }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/client/src/components/admin-panel/user-support/index.vue b/website/client/src/components/admin-panel/user-support/index.vue
index 0573ea9896..6d8e2c73ce 100644
--- a/website/client/src/components/admin-panel/user-support/index.vue
+++ b/website/client/src/components/admin-panel/user-support/index.vue
@@ -47,6 +47,16 @@
:reset-counter="resetCounter"
/>
+
+
+
+
- {{ item | displayValue }}
+
+ {{ item | displayValue }}
+
:
{{ item.key }} :
@@ -102,15 +104,26 @@