mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 07:07:35 +01:00
show user history in admin panel
This commit is contained in:
@@ -67,6 +67,11 @@
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<user-history
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<contributor-details
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
@@ -121,6 +126,7 @@ import Transactions from './transactions';
|
||||
import SubscriptionAndPerks from './subscriptionAndPerks';
|
||||
import CustomizationsOwned from './customizationsOwned.vue';
|
||||
import Achievements from './achievements.vue';
|
||||
import UserHistory from './userHistory.vue';
|
||||
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
@@ -135,6 +141,7 @@ export default {
|
||||
PrivilegesAndGems,
|
||||
ContributorDetails,
|
||||
Transactions,
|
||||
UserHistory,
|
||||
SubscriptionAndPerks,
|
||||
UserProfile,
|
||||
Achievements,
|
||||
|
||||
@@ -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>
|
||||
@@ -5,3 +5,9 @@ export async function searchUsers (store, payload) {
|
||||
const response = await axios.get(url);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ api.acceptQuest = {
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
await UserHistory.beginUserHistoryUpdate(user._id)
|
||||
await UserHistory.beginUserHistoryUpdate(user._id, req.headers['x-client'])
|
||||
.withQuestInviteResponse(group.quest.key, 'accept')
|
||||
.commit();
|
||||
},
|
||||
@@ -294,7 +294,7 @@ api.rejectQuest = {
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
await UserHistory.beginUserHistoryUpdate(user._id)
|
||||
await UserHistory.beginUserHistoryUpdate(user._id, req.headers['x-client'])
|
||||
.withQuestInviteResponse(group.quest.key, 'reject')
|
||||
.commit();
|
||||
},
|
||||
|
||||
@@ -504,7 +504,7 @@ api.buy = {
|
||||
await user.save();
|
||||
|
||||
if (type === 'armoire') {
|
||||
await UserHistory.beginUserHistoryUpdate(user._id)
|
||||
await UserHistory.beginUserHistoryUpdate(user._id, req.headers['x-client'])
|
||||
.withArmoire(buyRes[0].armoire.dropKey || 'experience')
|
||||
.commit();
|
||||
}
|
||||
@@ -601,7 +601,7 @@ api.buyArmoire = {
|
||||
}
|
||||
const buyArmoireResponse = await common.ops.buy(user, req, res.analytics);
|
||||
await user.save();
|
||||
await UserHistory.beginUserHistoryUpdate(user._id)
|
||||
await UserHistory.beginUserHistoryUpdate(user._id, req.headers['x-client'])
|
||||
.withArmoire(buyArmoireResponse[1].data.armoire.dropKey)
|
||||
.commit();
|
||||
res.respond(200, ...buyArmoireResponse);
|
||||
|
||||
@@ -2,6 +2,10 @@ import validator from 'validator';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as UserHistory } from '../../models/userHistory';
|
||||
import {
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
|
||||
const api = {};
|
||||
|
||||
@@ -21,7 +25,7 @@ const api = {};
|
||||
* @apiUse NoUser
|
||||
* @apiUse NotAdmin
|
||||
*/
|
||||
api.getHero = {
|
||||
api.searchHero = {
|
||||
method: 'GET',
|
||||
url: '/admin/search/:userIdentifier',
|
||||
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;
|
||||
|
||||
@@ -524,7 +524,7 @@ export async function cron (options = {}) {
|
||||
user.flags.cronCount += 1;
|
||||
trackCronAnalytics(analytics, user, _progress, options);
|
||||
|
||||
await UserHistory.beginUserHistoryUpdate(user._id)
|
||||
await UserHistory.beginUserHistoryUpdate(user._id, options.headers['x-client'])
|
||||
.withCron()
|
||||
.commit();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export const schema = new Schema({
|
||||
{
|
||||
_id: false,
|
||||
timestamp: { $type: Date, required: true },
|
||||
client: { $type: String, required: false },
|
||||
reward: { $type: String, required: true },
|
||||
},
|
||||
],
|
||||
@@ -24,6 +25,7 @@ export const schema = new Schema({
|
||||
{
|
||||
_id: false,
|
||||
timestamp: { $type: Date, required: true },
|
||||
client: { $type: String, required: false },
|
||||
quest: { $type: String, required: true },
|
||||
response: { $type: String, required: true },
|
||||
},
|
||||
@@ -32,6 +34,7 @@ export const schema = new Schema({
|
||||
{
|
||||
_id: false,
|
||||
timestamp: { $type: Date, required: true },
|
||||
client: { $type: String, required: false },
|
||||
},
|
||||
],
|
||||
}, {
|
||||
@@ -81,10 +84,11 @@ const commitUserHistoryUpdate = function commitUserHistoryUpdate (update) {
|
||||
).exec();
|
||||
};
|
||||
|
||||
model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID) {
|
||||
model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID, client=null) {
|
||||
return {
|
||||
userId: userID,
|
||||
data: {
|
||||
client,
|
||||
armoire: [],
|
||||
questInviteResponses: [],
|
||||
cron: [],
|
||||
@@ -92,6 +96,7 @@ model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID) {
|
||||
withArmoire: function withArmoire (reward) {
|
||||
this.data.armoire.push({
|
||||
timestamp: new Date(),
|
||||
client: this.data.client,
|
||||
reward,
|
||||
});
|
||||
return this;
|
||||
@@ -99,6 +104,7 @@ model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID) {
|
||||
withQuestInviteResponse: function withQuestInviteResponse (quest, response) {
|
||||
this.data.questInviteResponses.push({
|
||||
timestamp: new Date(),
|
||||
client: this.data.client,
|
||||
quest,
|
||||
response,
|
||||
});
|
||||
@@ -107,6 +113,7 @@ model.beginUserHistoryUpdate = function beginUserHistoryUpdate (userID) {
|
||||
withCron: function withCron () {
|
||||
this.data.cron.push({
|
||||
timestamp: new Date(),
|
||||
client: this.data.client,
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user