mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Add interface to block ip-addresses or clients due to abuse (#15484)
* Read IP blocks from database * begin building general blocking solution * add new frontend files * Add UI for managing blockers * correctly reset local data after creating blocker * Tweak wording * Add UI for managing blockers * restructure admin pages * improve test coverage * Improve blocker UI * add blocker to block emails from registration * lint fix * fix * lint fixes * fix import * add new permission for managing blockers * improve permission check * fix managing permissions from admin * improve navbar display for non fullAccess admin * update block error strings * lint fix * add option to errorHandler to skip logging * validate blocker value during input * improve blocker form display * chore(subproj): reconcile habitica-images * fix(scripts): use same Mongo version for dev/test * fix(whitespace): eof * documentation improvements * remove nconf import * remove old test --------- Co-authored-by: Kalista Payne <kalista@habitica.com> Co-authored-by: Kalista Payne <sabrecat@gmail.com>
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Party, Quest
|
||||
<span
|
||||
v-if="errorsOrWarningsExist"
|
||||
>- ERRORS / WARNINGS EXIST</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
>
|
||||
<p v-if="partyNotExistError">
|
||||
ERROR: User has a Party ID but that Party does not exist.
|
||||
If you are seeing a red error notification on screen now
|
||||
("<strong>Group with id ... not found</strong>"), it's refering to this issue.
|
||||
<br>Ask a database admin to delete the user's Party ID ({{ userPartyData._id }}).
|
||||
</p>
|
||||
<p
|
||||
v-if="questErrors"
|
||||
v-html="questErrors"
|
||||
></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>
|
||||
<strong v-else>User is not in a party.</strong>
|
||||
<div class="subsection-start">
|
||||
<p v-html="questStatus"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
function determineQuestStatus (self) {
|
||||
// Quest data is in the user doc and party doc. They can be out of sync.
|
||||
// Here we collate data from both sources, showing error messages if needed.
|
||||
|
||||
// First get data from the party's document.
|
||||
const groupQuestData = self.groupPartyData.quest;
|
||||
let questExists = false; // true if quest is active or in invitation stage
|
||||
let questIsActive = false; // true if quest's invitation stage is over
|
||||
let inviteStatusForUser = '';
|
||||
let expectedRsvpStatusForUser = false;
|
||||
let countOfQuestMembers = 0;
|
||||
if (self.userHasParty && groupQuestData) {
|
||||
questIsActive = groupQuestData.active;
|
||||
if (groupQuestData.members) countOfQuestMembers = Object.keys(groupQuestData.members).length;
|
||||
if (groupQuestData.key) {
|
||||
questExists = true;
|
||||
if (!countOfQuestMembers) {
|
||||
self.questErrors = 'ERROR: Quest is running or in invitation stage but has no participants.';
|
||||
} else if (groupQuestData.members[self.userId] === null) {
|
||||
inviteStatusForUser = 'pending';
|
||||
if (questIsActive) {
|
||||
self.questErrors = 'ERROR: Quest is running but user\'s invitation is still pending ("null") in quest object.';
|
||||
} else {
|
||||
expectedRsvpStatusForUser = true;
|
||||
}
|
||||
} else if (groupQuestData.members[self.userId] === false) {
|
||||
inviteStatusForUser = 'rejected';
|
||||
if (questIsActive) {
|
||||
self.questErrors = 'ERROR: Quest is running and user\'s invitation was rejected BUT '
|
||||
+ 'it wasn\'t cleared properly from the quest\'s data ("false"). '
|
||||
+ 'That shouldn\'t cause any problems though.';
|
||||
}
|
||||
} else if (groupQuestData.members[self.userId] === true) {
|
||||
inviteStatusForUser = 'accepted';
|
||||
} else if (questIsActive) {
|
||||
inviteStatusForUser = 'rejected OR not accepted before quest start OR user joined party after quest started';
|
||||
} else {
|
||||
inviteStatusForUser = 'missing';
|
||||
self.questErrors = 'ERROR: Quest is in invitation stage but user doesn\'t have an invitation '
|
||||
+ 'in the party\'s data ("quest.members" needs to be fixed).';
|
||||
}
|
||||
} else if (questIsActive) {
|
||||
self.questErrors = 'ERROR: Quest is running but there is no "key" to say which quest it is. '
|
||||
+ 'This means the other data and errors in this section are unreliable, '
|
||||
+ 'and there may be more errors not shown here.'
|
||||
+ 'Other errors here may tell you which key to add.'
|
||||
+ 'After fixing, check for more errors.';
|
||||
// @TODO display a similar message for when it happens during invitation stage
|
||||
}
|
||||
}
|
||||
if (self.questErrors) self.questErrors += '<br>';
|
||||
// from this point on, further quest errors need to be appended to that
|
||||
|
||||
let questStatus = '<p>';
|
||||
if (questExists) {
|
||||
questStatus = 'Quest exists and is ';
|
||||
if (questIsActive) {
|
||||
questStatus += 'running.<br>User is ';
|
||||
if (inviteStatusForUser !== 'accepted') questStatus += 'not ';
|
||||
questStatus += 'a participant.';
|
||||
} else {
|
||||
questStatus += 'in invitation stage.<br>'
|
||||
+ `User's invitation is ${inviteStatusForUser}.`;
|
||||
}
|
||||
questStatus += '<br>';
|
||||
if (!groupQuestData.leader) {
|
||||
self.questErrors += 'ERROR: quest does not have its owner specified '
|
||||
+ '(party needs value for "quest.leader").<br>';
|
||||
} else if (groupQuestData.leader === self.userId) {
|
||||
questStatus += 'User is the quest owner.';
|
||||
} else {
|
||||
questStatus += `Quest owner is ${groupQuestData.leader}`;
|
||||
}
|
||||
} else {
|
||||
questStatus = 'No quest.';
|
||||
}
|
||||
questStatus += '</p>';
|
||||
|
||||
// Assess quest participants.
|
||||
if (questExists && countOfQuestMembers) {
|
||||
const participants = (questIsActive) ? 'participants' : 'invitees';
|
||||
questStatus += `<p>Quest has ${countOfQuestMembers} ${participants}:<ul>`;
|
||||
for (const [memberId, inviteStatus] of Object.entries(groupQuestData.members)) {
|
||||
questStatus += '<li>';
|
||||
questStatus += (memberId === self.userId)
|
||||
? `@${self.username}`
|
||||
: memberId;
|
||||
let invitationDescription = '';
|
||||
const errMsg = ' - MINOR ERROR: this data should have been deleted when quest started';
|
||||
if (inviteStatus === true) {
|
||||
if (!questIsActive) invitationDescription = ' - invitation accepted';
|
||||
// we don't display anything if quest is running - obvious that participant accepted
|
||||
} else if (inviteStatus === false) {
|
||||
invitationDescription += ' - invitation rejected';
|
||||
if (questIsActive) invitationDescription += errMsg;
|
||||
} else {
|
||||
invitationDescription += ' - invitation pending';
|
||||
if (questIsActive) invitationDescription += errMsg;
|
||||
}
|
||||
questStatus += invitationDescription;
|
||||
questStatus += '</li>';
|
||||
}
|
||||
questStatus += '</ul></p>';
|
||||
// @TODO: show error if all invitations accepted but quest not active
|
||||
}
|
||||
|
||||
// Now get data from the user's document.
|
||||
if (!self.userPartyData.quest) self.userPartyData.quest = {};
|
||||
if (self.userPartyData.quest.RSVPNeeded !== expectedRsvpStatusForUser) {
|
||||
self.questErrors
|
||||
+= `ERROR: User's quest invitation ("party.quest.RSVPNeeded") should be "${expectedRsvpStatusForUser}" but isn't.<br>`;
|
||||
}
|
||||
|
||||
if (inviteStatusForUser === 'pending' || inviteStatusForUser === 'accepted') {
|
||||
if (!self.userPartyData.quest.key) {
|
||||
self.questErrors += 'ERROR: User has accepted quest invitation or invitation is '
|
||||
+ 'still pending but their account has no "key" for the quest.<br>';
|
||||
} else if (self.userPartyData.quest.key !== groupQuestData.key) {
|
||||
self.questErrors += 'ERROR: User has accepted quest invitation or invitation is '
|
||||
+ `still pending but the "key" in their account (${self.userPartyData.quest.key}) `
|
||||
+ `is different than the quest's "key" (${groupQuestData.key}).<br>`;
|
||||
}
|
||||
} else if (self.userPartyData.quest.key) {
|
||||
self.questErrors += `ERROR: User has a "key" for the quest (${self.userPartyData.quest.key})`
|
||||
+ 'but perhaps should not have (no quest exists, or user not participating, '
|
||||
+ 'or quest is in erroneous state).<br>';
|
||||
}
|
||||
|
||||
// Display details of quest (name, type, progress, etc).
|
||||
if (questExists) {
|
||||
const questContent = quests.quests[groupQuestData.key];
|
||||
if (questContent) {
|
||||
let questContentData = `<strong>Quest Details</strong>:<br>Quest name: ${questContent.text()}<br>Quest "key": ${questContent.key}`;
|
||||
let questProgress = '<strong>Quest Progress:</strong>';
|
||||
if (!questIsActive) questProgress += ' none (quest is in invitation stage)';
|
||||
let userProgressToday;
|
||||
let userMadeZeroProgress = false;
|
||||
if (questContent.boss) {
|
||||
// NB Data rounding below is done in the same way as on the user's party page.
|
||||
questContentData += `<br>Boss name: ${questContent.boss.name()}`
|
||||
+ `<br>Boss's starting HP: ${questContent.boss.hp}`
|
||||
+ `<br>Boss's Strength: ${questContent.boss.str}`;
|
||||
let bossHasRage;
|
||||
if (questContent.boss.rage && questContent.boss.rage.value) {
|
||||
bossHasRage = true;
|
||||
questContentData += `<br>Boss's rage name for this quest: ${questContent.boss.rage.title()}`;
|
||||
questContentData += `<br>Boss's rage limit: ${questContent.boss.rage.value}`;
|
||||
}
|
||||
if (questIsActive) {
|
||||
if (!groupQuestData.progress || groupQuestData.progress.hp === undefined) {
|
||||
self.questErrors += 'ERROR: Party\'s quest is missing some or all of the "progress" data.<br>';
|
||||
} else {
|
||||
questProgress += `<br>Current Boss HP: ${Math.ceil(groupQuestData.progress.hp * 100) / 100}`;
|
||||
}
|
||||
if (bossHasRage) {
|
||||
questProgress += `<br>Current Rage: ${Math.floor(groupQuestData.progress.rage * 100) / 100}`;
|
||||
}
|
||||
}
|
||||
userProgressToday = `Player's pending damage to Boss: ${Math.floor(self.userPartyData.quest.progress.up * 10) / 10}`;
|
||||
if (!self.userPartyData.quest.progress.up) userMadeZeroProgress = true;
|
||||
} else {
|
||||
questContentData += '<br>Need to collect:<ul>';
|
||||
if (questIsActive) questProgress += '<br>Current found items: <ul>';
|
||||
for (const [key, obj] of Object.entries(questContent.collect)) {
|
||||
questContentData += `<li>${obj.text()}: ${obj.count} ("key": ${key})</li>`;
|
||||
if (questIsActive) {
|
||||
if (!groupQuestData.progress || !groupQuestData.progress.collect) {
|
||||
self.questErrors += 'ERROR: Party\'s quest is missing some or all of the "progress" data.<br>';
|
||||
} else if (groupQuestData.progress.collect[key] !== undefined) {
|
||||
questProgress += `<li>${obj.text()}: ${groupQuestData.progress.collect[key]}</li>`;
|
||||
} else {
|
||||
self.questErrors += `ERROR: Party's quest has no entry for "${key}" `
|
||||
+ '("quest.progress.collect" needs to be fixed).<br>';
|
||||
}
|
||||
}
|
||||
}
|
||||
questContentData += '</ul>';
|
||||
if (questIsActive) questProgress += '</ul>';
|
||||
userProgressToday = `Player's pending collected items: ${self.userPartyData.quest.progress.collectedItems}`;
|
||||
if (!self.userPartyData.quest.progress.collectedItems) userMadeZeroProgress = true;
|
||||
}
|
||||
if (userMadeZeroProgress) userProgressToday += '<br>NB: Zero pending quest progress may be from an error in which the user\'s database document is missing the pending progress fields. That error can\'t be identified here because the API will apply default data. If the user claims to have made pending progress but none is showing for them, a database admin has to check that.';
|
||||
questStatus += `<p>${questContentData}</p>`
|
||||
+ `<p>${questProgress}</p>`
|
||||
+ `<p>${userProgressToday}</p>`;
|
||||
questStatus += `<p><strong>Raw Quest Data:</strong></p><pre>party: ${JSON.stringify(groupQuestData, null, ' ')}`
|
||||
+ `\nuser: ${JSON.stringify(self.userPartyData.quest, null, ' ')}</pre>`;
|
||||
} else {
|
||||
self.questErrors += `ERROR: quest "key" ${groupQuestData.key} does not match a known quest.`;
|
||||
}
|
||||
}
|
||||
return questStatus;
|
||||
}
|
||||
|
||||
function resetData (self) {
|
||||
self.questStatus = '';
|
||||
self.questErrors = '';
|
||||
self.errorsOrWarningsExist = false;
|
||||
self.expand = false;
|
||||
|
||||
if (self.partyNotExistError) {
|
||||
self.errorsOrWarningsExist = true;
|
||||
} else {
|
||||
self.userIsPartyLeader = self.groupPartyData.leader === self.userId;
|
||||
}
|
||||
|
||||
// check for quest errors even if party doesn't exist (user can have old quest data)
|
||||
self.questStatus = determineQuestStatus(self);
|
||||
if (self.questErrors) self.errorsOrWarningsExist = true;
|
||||
|
||||
self.expand = self.errorsOrWarningsExist;
|
||||
}
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
userHasParty: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
partyNotExistError: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
userPartyData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
groupPartyData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
userIsPartyLeader: false,
|
||||
questStatus: '',
|
||||
questErrors: '',
|
||||
errorsOrWarningsExist: false,
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
removeFromParty () {
|
||||
this.saveHero({
|
||||
hero: { _id: this.userId, removeFromParty: true },
|
||||
msg: 'Removed from party',
|
||||
reloadData: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user