mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
🧑💼🎛️ Overhaul (#15270)
* Add option to search for users by email or username in admin panel * Make Admin panel design more consistent * fix test * fix width of items * escape regex for searching users * load own user when pressing enter on empty field * add styling for warning buttons * improve sub styling * fix checkbox alignment in admin panel * Unify date preview display * Fix bottom button display * admin panel display improvements * remove autocannon file * search improvements * time travel button display fix * fix loading spinner * fix sorting * Split email search into multiple queries * fix email search * remove console * fix line break
This commit is contained in:
@@ -42,23 +42,23 @@ describe('content index', () => {
|
|||||||
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
|
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Releases pets gear when appropriate without needing restarting', () => {
|
it('Releases pets when appropriate without needing restarting', () => {
|
||||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||||
const junePets = content.petInfo;
|
const junePets = content.petInfo;
|
||||||
expect(junePets['Chameleon-Base']).to.not.exist;
|
expect(junePets['Chameleon-Base']).to.not.exist;
|
||||||
clock.restore();
|
clock.restore();
|
||||||
clock = sinon.useFakeTimers(new Date('2024-07-20'));
|
clock = sinon.useFakeTimers(new Date('2024-07-18'));
|
||||||
const julyPets = content.petInfo;
|
const julyPets = content.petInfo;
|
||||||
expect(julyPets['Chameleon-Base']).to.exist;
|
expect(julyPets['Chameleon-Base']).to.exist;
|
||||||
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
|
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Releases mounts gear when appropriate without needing restarting', () => {
|
it('Releases mounts when appropriate without needing restarting', () => {
|
||||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||||
const juneMounts = content.mountInfo;
|
const juneMounts = content.mountInfo;
|
||||||
expect(juneMounts['Chameleon-Base']).to.not.exist;
|
expect(juneMounts['Chameleon-Base']).to.not.exist;
|
||||||
clock.restore();
|
clock.restore();
|
||||||
clock = sinon.useFakeTimers(new Date('2024-07-20'));
|
clock = sinon.useFakeTimers(new Date('2024-07-18'));
|
||||||
const julyMounts = content.mountInfo;
|
const julyMounts = content.mountInfo;
|
||||||
expect(julyMounts['Chameleon-Base']).to.exist;
|
expect(julyMounts['Chameleon-Base']).to.exist;
|
||||||
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
|
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
|
||||||
|
|||||||
@@ -174,6 +174,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: $orange-10;
|
||||||
|
color: $white !important;
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(.disabled) {
|
||||||
|
background: $orange-100;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: $orange-10;
|
||||||
|
border-color: $purple-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: $purple-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
|
||||||
|
background: $orange-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-success {
|
.btn-success {
|
||||||
background: $green-50;
|
background: $green-50;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|||||||
@@ -1,30 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="row standard-page">
|
<div class="row standard-page col-12 d-flex justify-content-center">
|
||||||
<div class="well col-12">
|
<div class="admin-panel-content">
|
||||||
<h1>Admin Panel</h1>
|
<h1>Admin Panel</h1>
|
||||||
|
|
||||||
<div>
|
|
||||||
<form
|
<form
|
||||||
class="form-inline"
|
class="form-inline"
|
||||||
@submit.prevent="loadHero(userIdentifier)"
|
@submit.prevent="searchUsers(userIdentifier)"
|
||||||
>
|
>
|
||||||
|
<div class="input-group col pl-0 pr-0">
|
||||||
<input
|
<input
|
||||||
v-model="userIdentifier"
|
v-model="userIdentifier"
|
||||||
class="form-control uidField"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="'User ID or Username; blank for your account'"
|
:placeholder="'UserID, username, email, or leave blank for your account'"
|
||||||
>
|
>
|
||||||
<input
|
<div class="input-group-append">
|
||||||
type="submit"
|
<button
|
||||||
value="Load User"
|
class="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
@click="loadUser(userIdentifier)"
|
||||||
|
>
|
||||||
|
Load User
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="searchUsers(userIdentifier)"
|
||||||
>
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<router-view
|
||||||
<router-view @changeUserIdentifier="changeUserIdentifier" />
|
class="mt-3"
|
||||||
</div>
|
@changeUserIdentifier="changeUserIdentifier"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,6 +44,15 @@
|
|||||||
.uidField {
|
.uidField {
|
||||||
min-width: 45ch;
|
min-width: 45ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group-append {
|
||||||
|
width:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-panel-content {
|
||||||
|
flex: 0 0 800px;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -62,7 +82,24 @@ export default {
|
|||||||
// (useful if we want to re-fetch the user after making changes).
|
// (useful if we want to re-fetch the user after making changes).
|
||||||
this.userIdentifier = newId;
|
this.userIdentifier = newId;
|
||||||
},
|
},
|
||||||
async loadHero (userIdentifier) {
|
async searchUsers (userIdentifier) {
|
||||||
|
if (!userIdentifier || userIdentifier === '') {
|
||||||
|
this.loadUser();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$router.push({
|
||||||
|
name: 'adminPanelSearch',
|
||||||
|
params: { userIdentifier },
|
||||||
|
}).catch(failure => {
|
||||||
|
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||||
|
// the admin has requested that the same user be displayed again so reload the page
|
||||||
|
// (e.g., if they changed their mind about changes they were making)
|
||||||
|
this.$router.go();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadUser (userIdentifier) {
|
||||||
const id = userIdentifier || this.user._id;
|
const id = userIdentifier || this.user._id;
|
||||||
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
|
|||||||
155
website/client/src/components/admin-panel/search.vue
Normal file
155
website/client/src/components/admin-panel/search.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="noUsersFound"
|
||||||
|
class="alert alert-warning"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
Could not find any matching users.
|
||||||
|
</div>
|
||||||
|
<loading-spinner class="mx-auto mb-2" dark-color="true" v-if="isSearching" />
|
||||||
|
<div
|
||||||
|
v-if="users.length > 0"
|
||||||
|
class="list-group"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user._id"
|
||||||
|
href="#"
|
||||||
|
class="list-group-item list-group-item-action"
|
||||||
|
@click="loadUser(user._id)"
|
||||||
|
>
|
||||||
|
<div class="d-flex w-100 justify-content-between">
|
||||||
|
<h5 class="mb-1">{{ user.profile.name }}</h5>
|
||||||
|
<small>{{ user._id }}</small>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="mb-1"
|
||||||
|
:class="{'highlighted-value': matchValueToIdentifier(user.auth.local.username)}"
|
||||||
|
>
|
||||||
|
@{{ user.auth.local.username }}</p>
|
||||||
|
<p class="mb-0">
|
||||||
|
<span
|
||||||
|
v-for="email in userEmails(user)"
|
||||||
|
:key="email"
|
||||||
|
:class="{'highlighted-value': matchValueToIdentifier(email)}"
|
||||||
|
>
|
||||||
|
{{ email }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.highlighted-value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import VueRouter from 'vue-router';
|
||||||
|
import { mapState } from '@/libs/store';
|
||||||
|
import LoadingSpinner from '../ui/loadingSpinner';
|
||||||
|
|
||||||
|
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LoadingSpinner,
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
userIdentifier: '',
|
||||||
|
users: [],
|
||||||
|
noUsersFound: false,
|
||||||
|
isSearching: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({ user: 'user.data' }),
|
||||||
|
},
|
||||||
|
beforeRouteUpdate (to, from, next) {
|
||||||
|
this.userIdentifier = to.params.userIdentifier;
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
userIdentifier () {
|
||||||
|
this.isSearching = true;
|
||||||
|
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
|
||||||
|
this.isSearching = false;
|
||||||
|
if (users.length === 1) {
|
||||||
|
this.loadUser(users[0]._id);
|
||||||
|
} else {
|
||||||
|
const matchIndex = users.findIndex(user => this.isExactMatch(user));
|
||||||
|
if (matchIndex !== -1) {
|
||||||
|
users.splice(0, 0, users.splice(matchIndex, 1)[0]);
|
||||||
|
}
|
||||||
|
this.users = users;
|
||||||
|
this.noUsersFound = users.length === 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$emit('changeUserIdentifier', this.userIdentifier); // change user identifier in Admin Panel's form
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.userIdentifier = this.$route.params.userIdentifier;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
matchValueToIdentifier (value) {
|
||||||
|
return value.toLowerCase().includes(this.userIdentifier.toLowerCase());
|
||||||
|
},
|
||||||
|
userEmails (user) {
|
||||||
|
const allEmails = [];
|
||||||
|
if (user.auth.local.email) allEmails.push(user.auth.local.email);
|
||||||
|
if (user.auth.google && user.auth.google.emails) {
|
||||||
|
const emails = user.auth.google.emails;
|
||||||
|
allEmails.push(...this.findSocialEmails(emails));
|
||||||
|
}
|
||||||
|
if (user.auth.apple && user.auth.apple.emails) {
|
||||||
|
const emails = user.auth.apple.emails;
|
||||||
|
allEmails.push(...this.findSocialEmails(emails));
|
||||||
|
}
|
||||||
|
if (user.auth.facebook && user.auth.facebook.emails) {
|
||||||
|
const emails = user.auth.facebook.emails;
|
||||||
|
allEmails.push(...this.findSocialEmails(emails));
|
||||||
|
}
|
||||||
|
return allEmails;
|
||||||
|
},
|
||||||
|
findSocialEmails (emails) {
|
||||||
|
if (typeof emails === 'string') return [emails];
|
||||||
|
if (Array.isArray(emails)) return emails.map(email => email.value);
|
||||||
|
if (typeof emails === 'object') return [emails.value];
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
async loadUser (userIdentifier) {
|
||||||
|
const id = userIdentifier || this.user._id;
|
||||||
|
|
||||||
|
this.$router.push({
|
||||||
|
name: 'adminPanelUser',
|
||||||
|
params: { userIdentifier: id },
|
||||||
|
}).catch(failure => {
|
||||||
|
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||||
|
// the admin has requested that the same user be displayed again so reload the page
|
||||||
|
// (e.g., if they changed their mind about changes they were making)
|
||||||
|
this.$router.go();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isExactMatch (user) {
|
||||||
|
return user._id === this.userIdentifier
|
||||||
|
|| user.auth.local.username === this.userIdentifier
|
||||||
|
|| (user.auth.google && user.auth.google.emails && user.auth.google.emails.findIndex(
|
||||||
|
email => email.value === this.userIdentifier,
|
||||||
|
) !== -1)
|
||||||
|
|| (user.auth.apple && user.auth.apple.emails && user.auth.apple.emails.findIndex(
|
||||||
|
email => email.value === this.userIdentifier,
|
||||||
|
) !== -1)
|
||||||
|
|| (user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails.findIndex(
|
||||||
|
email => email.value === this.userIdentifier,
|
||||||
|
) !== -1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Achievements
|
Achievements
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="item in achievements"
|
v-for="item in achievements"
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Current Avatar Appearance, Drop Count Today
|
Current Avatar Appearance, Drop Count Today
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<div>Drops Today: {{ items.lastDrop.count }}</div>
|
<div>Drops Today: {{ items.lastDrop.count }}</div>
|
||||||
<div>Most Recent Drop: {{ items.lastDrop.date | formatDate }}</div>
|
<div>Most Recent Drop: {{ items.lastDrop.date | formatDate }}</div>
|
||||||
<div>Use Costume: {{ preferences.costume ? 'on' : 'off' }}</div>
|
<div>Use Costume: {{ preferences.costume ? 'on' : 'off' }}</div>
|
||||||
|
|||||||
@@ -1,79 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<form @submit.prevent="saveHero({ hero, msg: 'Contributor details', clearData: true })">
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{ 'open': expand }"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Contributor Details
|
Contributor Details
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
|
||||||
<form @submit.prevent="saveHero({hero, msg: 'Contributor details', clearData: true})">
|
|
||||||
<div>
|
|
||||||
<label>Permissions</label>
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
v-model="hero.permissions.fullAccess"
|
|
||||||
:disabled="!hasPermission(user, 'fullAccess')"
|
|
||||||
type="checkbox"
|
|
||||||
>
|
|
||||||
Full Admin Access (Allows access to everything. EVERYTHING)
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div
|
||||||
<label>
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="mt-0">
|
||||||
|
Permissions
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
v-for="permission in permissionList"
|
||||||
|
:key="permission.key"
|
||||||
|
class="col-sm-9 offset-sm-3"
|
||||||
|
>
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
<input
|
<input
|
||||||
v-model="hero.permissions.userSupport"
|
v-model="hero.permissions[permission.key]"
|
||||||
:disabled="!hasPermission(user, 'fullAccess')"
|
:disabled="!hasPermission(user, permission.key)"
|
||||||
|
class="custom-control-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
>
|
>
|
||||||
User Support (Access this form, access purchase history)
|
<label class="custom-control-label">
|
||||||
</label>
|
{{ permission.name }}<br>
|
||||||
</div>
|
<small class="text-secondary">{{ permission.description }}</small>
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
v-model="hero.permissions.news"
|
|
||||||
:disabled="!hasPermission(user, 'fullAccess')"
|
|
||||||
type="checkbox"
|
|
||||||
>
|
|
||||||
News poster (Bailey CMS)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
v-model="hero.permissions.moderator"
|
|
||||||
:disabled="!hasPermission(user, 'fullAccess')"
|
|
||||||
type="checkbox"
|
|
||||||
>
|
|
||||||
Community Moderator (ban and mute users, access chat flags, manage social spaces)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
v-model="hero.permissions.challengeAdmin"
|
|
||||||
:disabled="!hasPermission(user, 'fullAccess')"
|
|
||||||
type="checkbox"
|
|
||||||
>
|
|
||||||
Challenge Admin (can create official habitica challenges and admin all challenges)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
v-model="hero.permissions.coupons"
|
|
||||||
:disabled="!hasPermission(user, 'fullAccess')"
|
|
||||||
type="checkbox"
|
|
||||||
>
|
|
||||||
Coupon Creator (can manage coupon codes)
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label>Title</label>
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Title</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.contributor.text"
|
v-model="hero.contributor.text"
|
||||||
class="form-control textField"
|
class="form-control textField"
|
||||||
@@ -89,8 +55,10 @@
|
|||||||
Statistician, Tinker, Transcriber, Troubadour.
|
Statistician, Tinker, Transcriber, Troubadour.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group form-inline">
|
</div>
|
||||||
<label>Tier</label>
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Tier</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.contributor.level"
|
v-model="hero.contributor.level"
|
||||||
class="form-control levelField"
|
class="form-control levelField"
|
||||||
@@ -99,34 +67,28 @@
|
|||||||
<small>
|
<small>
|
||||||
1-7 for normal contributors, 8 for moderators, 9 for staff.
|
1-7 for normal contributors, 8 for moderators, 9 for staff.
|
||||||
This determines which items, pets, mounts are available, and name-tag coloring.
|
This determines which items, pets, mounts are available, and name-tag coloring.
|
||||||
Tiers 8 and 9 are automatically given admin status.
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="hero.secret.text"
|
|
||||||
class="form-group"
|
|
||||||
>
|
|
||||||
<label>Moderation Notes</label>
|
|
||||||
<div
|
|
||||||
v-markdown="hero.secret.text"
|
|
||||||
class="markdownPreview"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group row">
|
||||||
<label>Contributions</label>
|
<label class="col-sm-3 col-form-label">Contributions</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="hero.contributor.contributions"
|
v-model="hero.contributor.contributions"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
cols="5"
|
cols="5"
|
||||||
rows="5"
|
rows="5"
|
||||||
></textarea>
|
>
|
||||||
|
</textarea>
|
||||||
<div
|
<div
|
||||||
v-markdown="hero.contributor.contributions"
|
v-markdown="hero.contributor.contributions"
|
||||||
class="markdownPreview"
|
class="markdownPreview"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label>Edit Moderation Notes</label>
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Moderation Notes</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="hero.secret.text"
|
v-model="hero.secret.text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -138,23 +100,30 @@
|
|||||||
class="markdownPreview"
|
class="markdownPreview"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-footer"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save and Clear Data"
|
value="Save"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary mt-1"
|
||||||
>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.levelField {
|
.levelField {
|
||||||
min-width: 10ch;
|
min-width: 10ch;
|
||||||
}
|
}
|
||||||
.textField {
|
|
||||||
|
.textField {
|
||||||
min-width: 50ch;
|
min-width: 50ch;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -164,6 +133,39 @@ import saveHero from '../mixins/saveHero';
|
|||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import { userStateMixin } from '../../../mixins/userState';
|
import { userStateMixin } from '../../../mixins/userState';
|
||||||
|
|
||||||
|
const permissionList = [
|
||||||
|
{
|
||||||
|
key: 'fullAccess',
|
||||||
|
name: 'Full Admin Access',
|
||||||
|
description: 'Allows access to everything. EVERYTHING',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'userSupport',
|
||||||
|
name: 'User Support',
|
||||||
|
description: 'Access this form, access purchase history',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'news',
|
||||||
|
name: 'News Poster',
|
||||||
|
description: 'Bailey CMS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'moderator',
|
||||||
|
name: 'Community Moderator',
|
||||||
|
description: 'Ban and mute users, access chat flags, manage social spaces',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'challengeAdmin',
|
||||||
|
name: 'Challenge Admin',
|
||||||
|
description: 'Can create official habitica challenges and admin all challenges',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'coupons',
|
||||||
|
name: 'Coupon Creator',
|
||||||
|
description: 'Can manage coupon codes',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function resetData (self) {
|
function resetData (self) {
|
||||||
self.expand = self.hero.contributor.level;
|
self.expand = self.hero.contributor.level;
|
||||||
}
|
}
|
||||||
@@ -192,6 +194,7 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
expand: false,
|
expand: false,
|
||||||
|
permissionList,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<form @submit.prevent="saveHero({ hero, msg: 'Authentication' })">
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
@@ -10,7 +12,11 @@
|
|||||||
v-if="errorsOrWarningsExist"
|
v-if="errorsOrWarningsExist"
|
||||||
>- ERRORS / WARNINGS EXIST</span>
|
>- ERRORS / WARNINGS EXIST</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<p
|
<p
|
||||||
v-if="errorsOrWarningsExist"
|
v-if="errorsOrWarningsExist"
|
||||||
class="errorMessage"
|
class="errorMessage"
|
||||||
@@ -18,16 +24,22 @@
|
|||||||
See error(s) below.
|
See error(s) below.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div class="form-group row">
|
||||||
Account created:
|
<label class="col-sm-3 col-form-label">Account created:</label>
|
||||||
<strong>{{ hero.auth.timestamps.created | formatDate }}</strong>
|
<strong class="col-sm-9 col-form-label">
|
||||||
|
{{ hero.auth.timestamps.created | formatDate }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hero.flags.thirdPartyTools">
|
<div class="form-group row">
|
||||||
User has employed <strong>third party tools</strong>. Last known usage:
|
<label class="col-sm-3 col-form-label">Used third party tools:</label>
|
||||||
<strong>{{ hero.flags.thirdPartyTools | formatDate }}</strong>
|
|
||||||
|
<div class="col-sm-9 col-form-label">
|
||||||
|
<strong v-if="hero.flags.thirdPartyTools">
|
||||||
|
Yes - {{ hero.flags.thirdPartyTools | formatDate }}</strong>
|
||||||
|
<strong v-else>No</strong>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="cronError">
|
</div>
|
||||||
"lastCron" value:
|
<div v-if="cronError" class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">lastCron value:</label>
|
||||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||||
<br>
|
<br>
|
||||||
<span class="errorMessage">
|
<span class="errorMessage">
|
||||||
@@ -35,26 +47,34 @@
|
|||||||
("auth.timestamps.loggedin" and "lastCron" dates are different).
|
("auth.timestamps.loggedin" and "lastCron" dates are different).
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
<div class="form-group row">
|
||||||
<div>
|
<label class="col-sm-3 col-form-label">Most recent cron:</label>
|
||||||
Most recent cron:
|
|
||||||
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
<div class="col-sm-9 col-form-label">
|
||||||
("auth.timestamps.loggedin")
|
<strong>
|
||||||
</div>
|
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary ml-2"
|
class="btn btn-warning btn-sm ml-4"
|
||||||
@click="resetCron()"
|
@click="resetCron()"
|
||||||
>
|
>
|
||||||
Reset Cron to Yesterday
|
Reset Cron to Yesterday
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="subsection-start">
|
|
||||||
Time zone:
|
|
||||||
<strong>{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="form-group row">
|
||||||
Custom Day Start time (CDS):
|
<label class="col-sm-3 col-form-label">Time zone:</label>
|
||||||
<strong>{{ hero.preferences.dayStart }}</strong>
|
<strong class="col-sm-9 col-form-label">
|
||||||
|
{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Custom Day Start time (CDS)</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input
|
||||||
|
v-model="hero.preferences.dayStart"
|
||||||
|
class="form-control levelField"
|
||||||
|
type="number"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="timezoneDiffError || timezoneMissingError">
|
<div v-if="timezoneDiffError || timezoneMissingError">
|
||||||
Time zone at previous cron:
|
Time zone at previous cron:
|
||||||
@@ -87,18 +107,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="subsection-start form-inline">
|
<div class="form-group row">
|
||||||
API Token:
|
<label class="col-sm-3 col-form-label">API Token</label>
|
||||||
<form @submit.prevent="changeApiToken()">
|
<div class="col-sm-9">
|
||||||
<input
|
<button
|
||||||
type="submit"
|
|
||||||
value="Change API Token"
|
value="Change API Token"
|
||||||
class="btn btn-primary"
|
class="btn btn-danger"
|
||||||
|
@click="changeApiToken()"
|
||||||
>
|
>
|
||||||
</form>
|
Change API Token
|
||||||
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="tokenModified"
|
v-if="tokenModified"
|
||||||
class="form-inline"
|
|
||||||
>
|
>
|
||||||
<strong>API Token has been changed. Tell the player something like this:</strong>
|
<strong>API Token has been changed. Tell the player something like this:</strong>
|
||||||
<br>
|
<br>
|
||||||
@@ -112,34 +132,56 @@
|
|||||||
reboot your phone, then reinstall it.
|
reboot your phone, then reinstall it.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="subsection-start">
|
|
||||||
Local authentication:
|
|
||||||
<span v-if="hero.auth.local.email">Yes,
|
|
||||||
<strong>{{ hero.auth.local.email }}</strong></span>
|
|
||||||
<span v-else><strong>None</strong></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
Google authentication:
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Local Authentication E-Mail</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input
|
||||||
|
v-model="hero.auth.local.email"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Google authentication</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
|
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
|
||||||
<span v-else><strong>None</strong></span>
|
<span v-else><strong>None</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
Facebook authentication:
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Facebook authentication</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
|
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
|
||||||
<span v-else><strong>None</strong></span>
|
<span v-else><strong>None</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
Apple ID authentication:
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Apple ID authentication</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
|
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
|
||||||
<span v-else><strong>None</strong></span>
|
<span v-else><strong>None</strong></span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="subsection-start">
|
<div class="subsection-start">
|
||||||
Full "auth" object for checking above is correct:
|
Full "auth" object for checking above is correct:
|
||||||
<pre>{{ hero.auth }}</pre>
|
<pre>{{ hero.auth }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-footer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Save"
|
||||||
|
class="btn btn-primary mt-1"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Customizations
|
Customizations
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="itemType in itemTypes"
|
v-for="itemType in itemTypes"
|
||||||
:key="itemType"
|
:key="itemType"
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Items
|
Items
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
The sections below display each item's key (bolded if the player has ever owned it),
|
The sections below display each item's key (bolded if the player has ever owned it),
|
||||||
followed by the item's English name.
|
followed by the item's English name.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
@@ -10,7 +11,11 @@
|
|||||||
v-if="errorsOrWarningsExist"
|
v-if="errorsOrWarningsExist"
|
||||||
>- ERRORS / WARNINGS EXIST</span>
|
>- ERRORS / WARNINGS EXIST</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="errorsOrWarningsExist"
|
v-if="errorsOrWarningsExist"
|
||||||
class="errorMessage"
|
class="errorMessage"
|
||||||
|
|||||||
@@ -1,68 +1,107 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Privileges, Gem Balance
|
Priviliges, Gem Balance
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<p
|
<p
|
||||||
v-if="errorsOrWarningsExist"
|
v-if="errorsOrWarningsExist"
|
||||||
class="errorMessage"
|
class="errorMessage"
|
||||||
>
|
>
|
||||||
Player has had privileges removed or has moderation notes.
|
Player has had privileges removed or has moderation notes.
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
v-if="hero.flags"
|
v-if="hero.flags"
|
||||||
|
class="form-group row"
|
||||||
|
>
|
||||||
|
<div class="col-sm-9 offset-sm-3">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input
|
||||||
|
id="chatShadowMuted"
|
||||||
v-model="hero.flags.chatShadowMuted"
|
v-model="hero.flags.chatShadowMuted"
|
||||||
|
class="custom-control-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
> Shadow Mute
|
>
|
||||||
|
<label
|
||||||
|
class="custom-control-label"
|
||||||
|
for="chatShadowMuted"
|
||||||
|
>
|
||||||
|
Shadow Mute
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
</div>
|
||||||
<label>
|
</div>
|
||||||
<input
|
<div
|
||||||
v-if="hero.flags"
|
v-if="hero.flags"
|
||||||
v-model="hero.flags.chatRevoked"
|
class="form-group row"
|
||||||
type="checkbox"
|
>
|
||||||
> Mute (Revoke Chat Privileges)
|
<div class="col-sm-9 offset-sm-3">
|
||||||
</label>
|
<div class="custom-control custom-checkbox">
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
<input
|
||||||
v-model="hero.auth.blocked"
|
id="chatRevoked"
|
||||||
|
v-model="hero.flags.chatRevoked"
|
||||||
|
class="custom-control-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
> Ban / Block
|
>
|
||||||
|
<label
|
||||||
|
class="custom-control-label"
|
||||||
|
for="chatRevoked"
|
||||||
|
>
|
||||||
|
Mute (Revoke Chat Privileges)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
</div>
|
||||||
<label>
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-sm-9 offset-sm-3">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input
|
||||||
|
id="blocked"
|
||||||
|
v-model="hero.auth.blocked"
|
||||||
|
class="custom-control-input"
|
||||||
|
type="checkbox"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="custom-control-label"
|
||||||
|
for="blocked"
|
||||||
|
>
|
||||||
|
Ban / Block
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Balance
|
Balance
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.balance"
|
v-model="hero.balance"
|
||||||
class="form-control balanceField"
|
class="form-control balanceField"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.25"
|
step="0.25"
|
||||||
>
|
>
|
||||||
</label>
|
|
||||||
<span>
|
|
||||||
<small>
|
<small>
|
||||||
Balance is in USD, not in Gems.
|
Balance is in USD, not in Gems.
|
||||||
E.g., if this number is 1, it means 4 Gems.
|
E.g., if this number is 1, it means 4 Gems.
|
||||||
Arrows change Balance by 0.25 (i.e., 1 Gem per click).
|
Arrows change Balance by 0.25 (i.e., 1 Gem per click).
|
||||||
Do not use when awarding tiers; tier gems are automatic.
|
Do not use when awarding tiers; tier gems are automatic.
|
||||||
</small>
|
</small>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label>Moderation Notes</label>
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Moderation Notes</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="hero.secret.text"
|
v-model="hero.secret.text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -74,14 +113,20 @@
|
|||||||
class="markdownPreview"
|
class="markdownPreview"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-footer"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save"
|
value="Save"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary mt-1"
|
||||||
>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{ 'open': expand }"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Subscription, Monthly Perks
|
Subscription, Monthly Perks
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<div v-if="hero.purchased.plan.paymentMethod">
|
<div v-if="hero.purchased.plan.paymentMethod">
|
||||||
Payment method:
|
Payment method:
|
||||||
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
||||||
@@ -23,46 +28,72 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="hero.purchased.plan.dateCreated"
|
v-if="hero.purchased.plan.dateCreated"
|
||||||
class="form-inline"
|
class="form-group row"
|
||||||
>
|
>
|
||||||
<label>
|
<label class="col-sm-3 col-form-label">
|
||||||
Creation date:
|
Creation date:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.dateCreated"
|
v-model="hero.purchased.plan.dateCreated"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
> <strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateCreated) }}</strong>
|
>
|
||||||
</label>
|
<div class="input-group-append">
|
||||||
|
<strong class="input-group-text">
|
||||||
|
{{ dateFormat(hero.purchased.plan.dateCreated) }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="hero.purchased.plan.dateCurrentTypeCreated"
|
v-if="hero.purchased.plan.dateCurrentTypeCreated"
|
||||||
class="form-inline"
|
class="form-group row"
|
||||||
>
|
>
|
||||||
<label>
|
<label class="col-sm-3 col-form-label">
|
||||||
Start date for current subscription type:
|
Current sub start date:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.dateCurrentTypeCreated"
|
v-model="hero.purchased.plan.dateCurrentTypeCreated"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
>
|
||||||
</label>
|
<div class="input-group-append">
|
||||||
<strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}</strong>
|
<strong class="input-group-text">
|
||||||
|
{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
</div>
|
||||||
<label>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Termination date:
|
Termination date:
|
||||||
<div>
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.dateTerminated"
|
v-model="hero.purchased.plan.dateTerminated"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
> <strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateTerminated) }}</strong>
|
>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<strong class="input-group-text">
|
||||||
|
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
</div>
|
||||||
<label>
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Consecutive months:
|
Consecutive months:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.consecutive.count"
|
v-model="hero.purchased.plan.consecutive.count"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -70,11 +101,13 @@
|
|||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
>
|
>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
</div>
|
||||||
<label>
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Perk offset months:
|
Perk offset months:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.consecutive.offset"
|
v-model="hero.purchased.plan.consecutive.offset"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -82,10 +115,13 @@
|
|||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
>
|
>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Perk month count:
|
Perk month count:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.perkMonthCount"
|
v-model="hero.purchased.plan.perkMonthCount"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -95,13 +131,18 @@
|
|||||||
step="1"
|
step="1"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
Next Mystic Hourglass:
|
|
||||||
<strong>{{ nextHourglassDate }}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
<div class="form-group row">
|
||||||
<label>
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Next Mystic Hourglass:
|
||||||
|
</label>
|
||||||
|
<strong class="col-sm-9 col-form-label">{{ nextHourglassDate }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Mystic Hourglasses:
|
Mystic Hourglasses:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.consecutive.trinkets"
|
v-model="hero.purchased.plan.consecutive.trinkets"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -109,11 +150,13 @@
|
|||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
>
|
>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
</div>
|
||||||
<label>
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Gem cap increase:
|
Gem cap increase:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.consecutive.gemCapExtra"
|
v-model="hero.purchased.plan.consecutive.gemCapExtra"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -122,15 +165,21 @@
|
|||||||
max="25"
|
max="25"
|
||||||
step="5"
|
step="5"
|
||||||
>
|
>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Total Gem cap:
|
Total Gem cap:
|
||||||
<strong>{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 25 }}</strong>
|
</label>
|
||||||
|
<strong class="col-sm-9 col-form-label">
|
||||||
|
{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 25 }}
|
||||||
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
<div class="form-group row">
|
||||||
<label>
|
<label class="col-sm-3 col-form-label">
|
||||||
Gems bought this month:
|
Gems bought this month:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.purchased.plan.gemsBought"
|
v-model="hero.purchased.plan.gemsBought"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -139,19 +188,18 @@
|
|||||||
:max="hero.purchased.plan.consecutive.gemCapExtra + 25"
|
:max="hero.purchased.plan.consecutive.gemCapExtra + 25"
|
||||||
step="1"
|
step="1"
|
||||||
>
|
>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
v-if="hero.purchased.plan.extraMonths > 0"
|
<div v-if="hero.purchased.plan.extraMonths > 0">
|
||||||
>
|
|
||||||
Additional credit (applied upon cancellation):
|
Additional credit (applied upon cancellation):
|
||||||
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
|
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Mystery Items:
|
Mystery Items:
|
||||||
<span
|
</label>
|
||||||
v-if="hero.purchased.plan.mysteryItems.length > 0"
|
<div class="col-sm-9 col-form-label">
|
||||||
>
|
<span v-if="hero.purchased.plan.mysteryItems.length > 0">
|
||||||
<span
|
<span
|
||||||
v-for="(item, index) in hero.purchased.plan.mysteryItems"
|
v-for="(item, index) in hero.purchased.plan.mysteryItems"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -166,16 +214,38 @@
|
|||||||
<strong>None</strong>
|
<strong>None</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-footer"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save"
|
value="Save"
|
||||||
class="btn btn-primary mt-1"
|
class="btn btn-primary mt-1"
|
||||||
>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~@/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.input-group-append {
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { getPlanContext } from '@/../../common/script/cron';
|
import { getPlanContext } from '@/../../common/script/cron';
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="toggleTransactionsOpen"
|
@click="toggleTransactionsOpen"
|
||||||
>
|
>
|
||||||
Transactions
|
Transactions
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
<purchase-history-table
|
<purchase-history-table
|
||||||
:gem-transactions="gemTransactions"
|
:gem-transactions="gemTransactions"
|
||||||
:hourglass-transactions="hourglassTransactions"
|
:hourglass-transactions="hourglassTransactions"
|
||||||
|
|||||||
@@ -1,52 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="accordion-group">
|
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
class="expand-toggle"
|
class="mb-0 mt-0"
|
||||||
:class="{'open': expand}"
|
:class="{'open': expand}"
|
||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Users Profile
|
User Profile
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="expand">
|
</div>
|
||||||
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
|
<div
|
||||||
<div class="form-group">
|
v-if="expand"
|
||||||
<label>Display name</label>
|
class="card-body"
|
||||||
|
>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Display name</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.profile.name"
|
v-model="hero.profile.name"
|
||||||
class="form-control textField"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label>Photo URL</label>
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Photo URL</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<input
|
<input
|
||||||
v-model="hero.profile.imageUrl"
|
v-model="hero.profile.imageUrl"
|
||||||
class="form-control textField"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label>About</label>
|
<div class="form-group row">
|
||||||
<div class="row about-row">
|
<label class="col-sm-3 col-form-label">About</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="hero.profile.blurb"
|
v-model="hero.profile.blurb"
|
||||||
class="form-control col"
|
class="form-control"
|
||||||
rows="10"
|
rows="10"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div
|
<div
|
||||||
v-markdown="hero.profile.blurb"
|
v-markdown="hero.profile.blurb"
|
||||||
class="markdownPreview col"
|
class="markdownPreview"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-footer"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save"
|
value="Save"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary mt-1"
|
||||||
>
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -291,6 +291,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
class="time-travel"
|
||||||
v-if="TIME_TRAVEL_ENABLED && user.permissions && user.permissions.fullAccess"
|
v-if="TIME_TRAVEL_ENABLED && user.permissions && user.permissions.fullAccess"
|
||||||
:key="lastTimeJump"
|
:key="lastTimeJump"
|
||||||
>
|
>
|
||||||
@@ -309,9 +310,11 @@
|
|||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
Time Traveling! It is {{ new Date().toLocaleDateString() }}
|
Time Traveling! It is {{ new Date().toLocaleDateString() }}
|
||||||
<a
|
<a
|
||||||
class="btn btn-warning mr-1"
|
class="btn btn-warning btn-small"
|
||||||
@click="resetTime()"
|
@click="resetTime()"
|
||||||
>Reset</a>
|
>
|
||||||
|
Reset
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
class="btn btn-secondary mr-1"
|
class="btn btn-secondary mr-1"
|
||||||
@@ -510,6 +513,8 @@ li {
|
|||||||
grid-area: debug-pop;
|
grid-area: debug-pop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-travel { grid-area: time-travel;}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background-color: $gray-500;
|
background-color: $gray-500;
|
||||||
color: $gray-50;
|
color: $gray-50;
|
||||||
@@ -530,7 +535,7 @@ footer {
|
|||||||
"donate-text donate-text donate-text donate-button social"
|
"donate-text donate-text donate-text donate-button social"
|
||||||
"hr hr hr hr hr"
|
"hr hr hr hr hr"
|
||||||
"copyright copyright melior privacy-terms privacy-terms"
|
"copyright copyright melior privacy-terms privacy-terms"
|
||||||
"debug-toggle debug-toggle debug-toggle blank blank";
|
"time-travel time-travel debug-toggle debug-toggle debug-toggle";
|
||||||
grid-template-columns: repeat(5, 1fr);
|
grid-template-columns: repeat(5, 1fr);
|
||||||
grid-template-rows: auto;
|
grid-template-rows: auto;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
v-once
|
v-once
|
||||||
class="loading-spinner"
|
class="loading-spinner"
|
||||||
|
:class="{'loading-spinner-purple': darkColor}"
|
||||||
role="text"
|
role="text"
|
||||||
:aria-label="$t('loading')"
|
:aria-label="$t('loading')"
|
||||||
>
|
>
|
||||||
@@ -39,6 +40,10 @@
|
|||||||
border-color: $white transparent transparent transparent;
|
border-color: $white transparent transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-spinner-purple div {
|
||||||
|
border-color: $purple-200 transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-spinner div:nth-child(1) {
|
.loading-spinner div:nth-child(1) {
|
||||||
animation-delay: -0.45s;
|
animation-delay: -0.45s;
|
||||||
}
|
}
|
||||||
@@ -58,3 +63,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
darkColor: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -318,13 +318,18 @@
|
|||||||
color: $gray-50;
|
color: $gray-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td span {
|
||||||
|
line-break: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
padding-top: 0.35rem !important;
|
padding-top: 0.35rem !important;
|
||||||
padding-bottom: 0.35rem !important;
|
padding-bottom: 0.35rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timestamp-column, .action-column {
|
.timestamp-column, .action-column {
|
||||||
width: 20%;
|
width: 27%;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount-column {
|
.amount-column {
|
||||||
@@ -332,7 +337,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-column {
|
.note-column {
|
||||||
width: 50%;
|
width: 35%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry-action {
|
.entry-action {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall
|
|||||||
// Admin Panel
|
// Admin Panel
|
||||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel');
|
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel');
|
||||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/user-support');
|
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/user-support');
|
||||||
|
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/search');
|
||||||
|
|
||||||
// Except for tasks that are always loaded all the other main level
|
// Except for tasks that are always loaded all the other main level
|
||||||
// All the main level
|
// All the main level
|
||||||
@@ -193,9 +194,19 @@ const router = new VueRouter({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
name: 'adminPanelSearch',
|
||||||
|
path: 'search/:userIdentifier',
|
||||||
|
component: AdminPanelSearchPage,
|
||||||
|
meta: {
|
||||||
|
privilegeNeeded: [
|
||||||
|
'userSupport',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'adminPanelUser',
|
name: 'adminPanelUser',
|
||||||
path: ':userIdentifier', // User ID or Username
|
path: ':userIdentifier',
|
||||||
component: AdminPanelUserPage,
|
component: AdminPanelUserPage,
|
||||||
meta: {
|
meta: {
|
||||||
privilegeNeeded: [
|
privilegeNeeded: [
|
||||||
|
|||||||
7
website/client/src/store/actions/adminPanel.js
Normal file
7
website/client/src/store/actions/adminPanel.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export async function searchUsers (store, payload) {
|
||||||
|
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { flattenAndNamespace } from '@/libs/store/helpers/internals';
|
import { flattenAndNamespace } from '@/libs/store/helpers/internals';
|
||||||
|
|
||||||
|
import * as adminPanel from './adminPanel';
|
||||||
import * as common from './common';
|
import * as common from './common';
|
||||||
import * as user from './user';
|
import * as user from './user';
|
||||||
import * as tasks from './tasks';
|
import * as tasks from './tasks';
|
||||||
@@ -24,6 +25,7 @@ import * as faq from './faq';
|
|||||||
// Example: fetch in user.js -> 'user:fetch'
|
// Example: fetch in user.js -> 'user:fetch'
|
||||||
|
|
||||||
const actions = flattenAndNamespace({
|
const actions = flattenAndNamespace({
|
||||||
|
adminPanel,
|
||||||
common,
|
common,
|
||||||
user,
|
user,
|
||||||
tasks,
|
tasks,
|
||||||
|
|||||||
@@ -389,14 +389,19 @@ api.updateHero = {
|
|||||||
hero.markModified('items');
|
hero.markModified('items');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateData.auth && updateData.auth.blocked === true) {
|
if (updateData.auth) {
|
||||||
|
if (updateData.auth.blocked === true) {
|
||||||
hero.auth.blocked = updateData.auth.blocked;
|
hero.auth.blocked = updateData.auth.blocked;
|
||||||
hero.preferences.sleep = true; // when blocking, have them rest at an inn to prevent damage
|
hero.preferences.sleep = true; // when blocking, have them rest at an inn to prevent damage
|
||||||
}
|
} else if (updateData.auth.blocked === false) {
|
||||||
if (updateData.auth && updateData.auth.blocked === false) {
|
|
||||||
hero.auth.blocked = false;
|
hero.auth.blocked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateData.auth.local && updateData.auth.local.email) {
|
||||||
|
hero.auth.local.email = updateData.auth.local.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) {
|
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) {
|
||||||
hero.flags.chatRevoked = updateData.flags.chatRevoked;
|
hero.flags.chatRevoked = updateData.flags.chatRevoked;
|
||||||
}
|
}
|
||||||
|
|||||||
76
website/server/controllers/api-v4/admin.js
Normal file
76
website/server/controllers/api-v4/admin.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import validator from 'validator';
|
||||||
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
|
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||||
|
import { model as User } from '../../models/user';
|
||||||
|
|
||||||
|
const api = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /api/v4/admin/search/:userIdentifier Search for users by username or email
|
||||||
|
* @apiParam (Path) {String} userIdentifier The username or email of the user to search for
|
||||||
|
* @apiName SearchUsers
|
||||||
|
* @apiGroup Admin
|
||||||
|
* @apiPermission Admin
|
||||||
|
*
|
||||||
|
* @apiDescription Returns a list of users that match the search criteria
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data The User list
|
||||||
|
*
|
||||||
|
* @apiUse NoAuthHeaders
|
||||||
|
* @apiUse NoAccount
|
||||||
|
* @apiUse NoUser
|
||||||
|
* @apiUse NotAdmin
|
||||||
|
*/
|
||||||
|
api.getHero = {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/admin/search/:userIdentifier',
|
||||||
|
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||||
|
async handler (req, res) {
|
||||||
|
req.checkParams('userIdentifier', res.t('userIdentifierRequired')).notEmpty();
|
||||||
|
|
||||||
|
const validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
const { userIdentifier } = req.params;
|
||||||
|
|
||||||
|
const re = new RegExp(String.raw`^${userIdentifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||||
|
|
||||||
|
let query;
|
||||||
|
let users = [];
|
||||||
|
if (validator.isUUID(userIdentifier)) {
|
||||||
|
query = { _id: userIdentifier };
|
||||||
|
} else if (validator.isEmail(userIdentifier)) {
|
||||||
|
const emailFields = [
|
||||||
|
'auth.local.email',
|
||||||
|
'auth.google.emails.value',
|
||||||
|
'auth.apple.emails.value',
|
||||||
|
'auth.facebook.emails.value',
|
||||||
|
];
|
||||||
|
for (const field of emailFields) {
|
||||||
|
const emailQuery = { [field]: userIdentifier };
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const found = await User.findOne(emailQuery)
|
||||||
|
.select('contributor backer profile auth')
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
if (found) {
|
||||||
|
users.push(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = { 'auth.local.lowerCaseUsername': { $regex: re, $options: 'i' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
users = await User
|
||||||
|
.find(query)
|
||||||
|
.select('contributor backer profile auth')
|
||||||
|
.limit(30)
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
res.respond(200, users);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
Reference in New Issue
Block a user