show user history in admin panel

This commit is contained in:
Phillip Thelen
2024-08-28 13:42:19 +02:00
parent 38cad7102f
commit 0bd0ba096b
8 changed files with 292 additions and 7 deletions

View File

@@ -67,6 +67,11 @@
:reset-counter="resetCounter" :reset-counter="resetCounter"
/> />
<user-history
:hero="hero"
:reset-counter="resetCounter"
/>
<contributor-details <contributor-details
:hero="hero" :hero="hero"
:reset-counter="resetCounter" :reset-counter="resetCounter"
@@ -121,6 +126,7 @@ import Transactions from './transactions';
import SubscriptionAndPerks from './subscriptionAndPerks'; import SubscriptionAndPerks from './subscriptionAndPerks';
import CustomizationsOwned from './customizationsOwned.vue'; import CustomizationsOwned from './customizationsOwned.vue';
import Achievements from './achievements.vue'; import Achievements from './achievements.vue';
import UserHistory from './userHistory.vue';
import { userStateMixin } from '../../../mixins/userState'; import { userStateMixin } from '../../../mixins/userState';
@@ -135,6 +141,7 @@ export default {
PrivilegesAndGems, PrivilegesAndGems,
ContributorDetails, ContributorDetails,
Transactions, Transactions,
UserHistory,
SubscriptionAndPerks, SubscriptionAndPerks,
UserProfile, UserProfile,
Achievements, Achievements,

View File

@@ -0,0 +1,222 @@
<template>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="toggleHistoryOpen"
>
User History
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div>
<div class="clearfix">
<div class="mb-4 float-left">
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'armoire'}"
@click="selectTab('armoire')"
>
Armoire
</button>
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'questInvites'}"
@click="selectTab('questInvites')"
>
Quest Invitations
</button>
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'cron'}"
@click="selectTab('cron')"
>
Cron
</button>
</div>
</div>
<div class="row">
<div
v-if="selectedTab === 'armoire'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>Client</th>
<th
v-once
>
Received
</th>
</tr>
<tr
v-for="entry in armoire"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
<td>{{ entry.reward }}</td>
</tr>
</table>
</div>
<div
v-if="selectedTab === 'questInvites'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>Client</th>
<th v-once>Quest Key</th>
<th v-once>Response</th>
</tr>
<tr
v-for="entry in questInvites"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
<td>{{ entry.quest }}</td>
<td>{{ entry.response }}</td>
</tr>
</table>
</div>
<div
v-if="selectedTab === 'cron'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>Client</th>
</tr>
<tr
v-for="entry in cron"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.page-header.btn-flat {
background: transparent;
}
.tab-button {
height: 2rem;
font-size: 24px;
font-weight: bold;
font-stretch: condensed;
line-height: 1.33;
letter-spacing: normal;
color: $gray-10;
margin-right: 1.125rem;
padding-left: 0;
padding-right: 0;
padding-bottom: 2.5rem;
&.active, &:hover {
color: $purple-300;
box-shadow: 0px -0.25rem 0px $purple-300 inset;
outline: none;
}
}
</style>
<script>
import moment from 'moment';
import { userStateMixin } from '../../../mixins/userState';
export default {
mixins: [userStateMixin],
props: {
hero: {
type: Object,
required: true,
},
resetCounter: {
type: Number,
required: true,
},
},
data () {
return {
expand: false,
selectedTab: 'armoire',
armoire: [],
questInviteResponses: [],
cron: [],
};
},
filters: {
timeAgo (value) {
return moment(value).fromNow();
},
},
watch: {
resetCounter () {
if (this.expand) {
this.retrieveUserHistory();
}
},
},
methods: {
selectTab (type) {
this.selectedTab = type;
},
async toggleHistoryOpen () {
this.expand = !this.expand;
if (this.expand) {
this.retrieveUserHistory();
}
},
async retrieveUserHistory () {
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
this.armoire = history.armoire;
this.questInviteResponses = history.questInviteResponses;
this.cron = history.cron;
},
},
};
</script>

View File

@@ -5,3 +5,9 @@ export async function searchUsers (store, payload) {
const response = await axios.get(url); const response = await axios.get(url);
return response.data.data; return response.data.data;
} }
export async function getUserHistory (store, payload) {
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
const response = await axios.get(url);
return response.data.data;
}

View File

@@ -229,7 +229,7 @@ api.acceptQuest = {
headers: req.headers, headers: req.headers,
}); });
await UserHistory.beginUserHistoryUpdate(user._id) await UserHistory.beginUserHistoryUpdate(user._id, req.headers['x-client'])
.withQuestInviteResponse(group.quest.key, 'accept') .withQuestInviteResponse(group.quest.key, 'accept')
.commit(); .commit();
}, },
@@ -294,7 +294,7 @@ api.rejectQuest = {
headers: req.headers, headers: req.headers,
}); });
await UserHistory.beginUserHistoryUpdate(user._id) await UserHistory.beginUserHistoryUpdate(user._id, req.headers['x-client'])
.withQuestInviteResponse(group.quest.key, 'reject') .withQuestInviteResponse(group.quest.key, 'reject')
.commit(); .commit();
}, },

View File

@@ -504,7 +504,7 @@ api.buy = {
await user.save(); await user.save();
if (type === 'armoire') { if (type === 'armoire') {
await UserHistory.beginUserHistoryUpdate(user._id) await UserHistory.beginUserHistoryUpdate(user._id, req.headers['x-client'])
.withArmoire(buyRes[0].armoire.dropKey || 'experience') .withArmoire(buyRes[0].armoire.dropKey || 'experience')
.commit(); .commit();
} }
@@ -601,7 +601,7 @@ api.buyArmoire = {
} }
const buyArmoireResponse = await common.ops.buy(user, req, res.analytics); const buyArmoireResponse = await common.ops.buy(user, req, res.analytics);
await user.save(); await user.save();
await UserHistory.beginUserHistoryUpdate(user._id) await UserHistory.beginUserHistoryUpdate(user._id, req.headers['x-client'])
.withArmoire(buyArmoireResponse[1].data.armoire.dropKey) .withArmoire(buyArmoireResponse[1].data.armoire.dropKey)
.commit(); .commit();
res.respond(200, ...buyArmoireResponse); res.respond(200, ...buyArmoireResponse);

View File

@@ -2,6 +2,10 @@ import validator from 'validator';
import { authWithHeaders } from '../../middlewares/auth'; import { authWithHeaders } from '../../middlewares/auth';
import { ensurePermission } from '../../middlewares/ensureAccessRight'; import { ensurePermission } from '../../middlewares/ensureAccessRight';
import { model as User } from '../../models/user'; import { model as User } from '../../models/user';
import { model as UserHistory } from '../../models/userHistory';
import {
NotFound,
} from '../../libs/errors';
const api = {}; const api = {};
@@ -21,7 +25,7 @@ const api = {};
* @apiUse NoUser * @apiUse NoUser
* @apiUse NotAdmin * @apiUse NotAdmin
*/ */
api.getHero = { api.searchHero = {
method: 'GET', method: 'GET',
url: '/admin/search/:userIdentifier', url: '/admin/search/:userIdentifier',
middlewares: [authWithHeaders(), ensurePermission('userSupport')], middlewares: [authWithHeaders(), ensurePermission('userSupport')],
@@ -73,4 +77,43 @@ api.getHero = {
}, },
}; };
/**
* @api {get} /api/v4/admin/user/:userId/history Get the history of a user
* @apiParam (Path) {String} userIdentifier The username or email of the user
* @apiName GetUserHistory
* @apiGroup Admin
* @apiPermission Admin
*
* @apiDescription Returns the history of a user
*
* @apiSuccess {Object} data The User history
*
* @apiUse NoAuthHeaders
* @apiUse NoAccount
* @apiUse NoUser
* @apiUse NotAdmin
*/
api.getUserHistory = {
method: 'GET',
url: '/admin/user/:userId/history',
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
async handler (req, res) {
req.checkParams('userId', res.t('heroIdRequired')).notEmpty().isUUID();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const { userId } = req.params;
const history = await UserHistory
.findOne({ userId })
.lean()
.exec();
if (!history) throw new NotFound(res.t('userWithIDNotFound', { userId }));
res.respond(200, history);
},
};
export default api; export default api;

View File

@@ -524,7 +524,7 @@ export async function cron (options = {}) {
user.flags.cronCount += 1; user.flags.cronCount += 1;
trackCronAnalytics(analytics, user, _progress, options); trackCronAnalytics(analytics, user, _progress, options);
await UserHistory.beginUserHistoryUpdate(user._id) await UserHistory.beginUserHistoryUpdate(user._id, options.headers['x-client'])
.withCron() .withCron()
.commit(); .commit();

View File

@@ -17,6 +17,7 @@ export const schema = new Schema({
{ {
_id: false, _id: false,
timestamp: { $type: Date, required: true }, timestamp: { $type: Date, required: true },
client: { $type: String, required: false },
reward: { $type: String, required: true }, reward: { $type: String, required: true },
}, },
], ],
@@ -24,6 +25,7 @@ export const schema = new Schema({
{ {
_id: false, _id: false,
timestamp: { $type: Date, required: true }, timestamp: { $type: Date, required: true },
client: { $type: String, required: false },
quest: { $type: String, required: true }, quest: { $type: String, required: true },
response: { $type: String, required: true }, response: { $type: String, required: true },
}, },
@@ -32,6 +34,7 @@ export const schema = new Schema({
{ {
_id: false, _id: false,
timestamp: { $type: Date, required: true }, timestamp: { $type: Date, required: true },
client: { $type: String, required: false },
}, },
], ],
}, { }, {
@@ -81,10 +84,11 @@ const commitUserHistoryUpdate = function commitUserHistoryUpdate (update) {
).exec(); ).exec();
}; };
model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID) { model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID, client=null) {
return { return {
userId: userID, userId: userID,
data: { data: {
client,
armoire: [], armoire: [],
questInviteResponses: [], questInviteResponses: [],
cron: [], cron: [],
@@ -92,6 +96,7 @@ model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID) {
withArmoire: function withArmoire (reward) { withArmoire: function withArmoire (reward) {
this.data.armoire.push({ this.data.armoire.push({
timestamp: new Date(), timestamp: new Date(),
client: this.data.client,
reward, reward,
}); });
return this; return this;
@@ -99,6 +104,7 @@ model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID) {
withQuestInviteResponse: function withQuestInviteResponse (quest, response) { withQuestInviteResponse: function withQuestInviteResponse (quest, response) {
this.data.questInviteResponses.push({ this.data.questInviteResponses.push({
timestamp: new Date(), timestamp: new Date(),
client: this.data.client,
quest, quest,
response, response,
}); });
@@ -107,6 +113,7 @@ model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID) {
withCron: function withCron () { withCron: function withCron () {
this.data.cron.push({ this.data.cron.push({
timestamp: new Date(), timestamp: new Date(),
client: this.data.client,
}); });
return this; return this;
}, },