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"
|
: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,
|
||||||
|
|||||||
@@ -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);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user