mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 07:37:25 +01:00
restructure admin pages
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="row standard-page col-12 d-flex justify-content-center">
|
||||
<div class="admin-panel-content">
|
||||
<h1>Admin Panel</h1>
|
||||
<h1>{{ $t("adminPanel") }}</h1>
|
||||
<form
|
||||
class="form-inline"
|
||||
@submit.prevent="searchUsers(userIdentifier)"
|
||||
@@ -72,7 +72,7 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: 'Admin Panel',
|
||||
section: this.$t('adminPanel'),
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
@@ -147,7 +147,7 @@ import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
const permissionList = [
|
||||
{
|
||||
@@ -149,7 +149,7 @@ import Achievements from './achievements.vue';
|
||||
import UserHistory from './userHistory.vue';
|
||||
import Stats from './stats.vue';
|
||||
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -189,7 +189,7 @@ import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
import StatsRow from './stats-row';
|
||||
|
||||
@@ -421,7 +421,7 @@ import isUUID from 'validator/es/lib/isUUID';
|
||||
import moment from 'moment';
|
||||
import { getPlanContext } from '@/../../common/script/cron';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import subscriptionBlocks from '../../../../../common/script/content/subscriptionBlocks';
|
||||
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
export default {
|
||||
@@ -22,8 +22,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PurchaseHistoryTable from '../../ui/purchaseHistoryTable.vue';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import PurchaseHistoryTable from '../../../ui/purchaseHistoryTable.vue';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -180,7 +180,7 @@
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
@@ -86,7 +86,7 @@ import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
|
||||
function resetData (self) {
|
||||
self.expand = false;
|
||||
@@ -3,12 +3,11 @@
|
||||
<td></td>
|
||||
<td><select class="form-control" v-model="blocker.type">
|
||||
<option value="ipaddress">IP-Address</option>
|
||||
<option value="email">E-Mail</option>
|
||||
<option value="client">Client Identifier</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><select class="form-control" v-model="blocker.area">
|
||||
<option value="full">Full</option>
|
||||
<option value="payments">Payments</option>
|
||||
</select></td>
|
||||
<td><input v-model="blocker.value"></td>
|
||||
<td><input v-model="blocker.reason"></td>
|
||||
@@ -101,7 +101,7 @@ export default {
|
||||
showCreateForm: false,
|
||||
newBlocker: {
|
||||
type: '',
|
||||
area: '',
|
||||
area: 'full',
|
||||
value: '',
|
||||
reason: '',
|
||||
},
|
||||
@@ -118,7 +118,7 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: 'Admin Panel',
|
||||
section: this.$t('siteBlockers'),
|
||||
});
|
||||
this.loadBlockers();
|
||||
},
|
||||
@@ -144,15 +144,23 @@ export default {
|
||||
async createBlocker (blocker) {
|
||||
await this.$store.dispatch('blockers:createBlocker', { blocker });
|
||||
this.showCreateForm = false;
|
||||
this.newBlocker = {
|
||||
type: '',
|
||||
area: 'full',
|
||||
value: '',
|
||||
reason: '',
|
||||
};
|
||||
this.loadBlockers();
|
||||
},
|
||||
|
||||
getTypeName (type) {
|
||||
switch (type) {
|
||||
case 'ipaddress':
|
||||
return 'IP Address';
|
||||
return 'IP-Address';
|
||||
case 'email':
|
||||
return 'E-Mail';
|
||||
case 'client':
|
||||
return 'Client Identifier';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
30
website/client/src/components/admin/container.vue
Normal file
30
website/client/src/components/admin/container.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<secondary-menu class="col-12">
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
{{ $t('adminPanel') }}
|
||||
</router-link><router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'blockers'}"
|
||||
>
|
||||
{{ $t('siteBlockers') }}
|
||||
</router-link>
|
||||
</secondary-menu><div class="col-12">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SecondaryMenu from '@/components/secondaryMenu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecondaryMenu,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -286,7 +286,7 @@
|
||||
:to="{ name: 'adminPanelUser',
|
||||
params: { userIdentifier: hero._id } }"
|
||||
>
|
||||
admin panel
|
||||
{{ $t("adminPanel") }}
|
||||
</router-link>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -295,14 +295,6 @@
|
||||
{{ $t('help') }}
|
||||
</router-link>
|
||||
<div class="topbar-dropdown">
|
||||
<router-link
|
||||
v-if="user.permissions.fullAccess ||
|
||||
user.permissions.userSupport"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
Admin Panel
|
||||
</router-link>
|
||||
<router-link
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'faq'}"
|
||||
@@ -336,6 +328,51 @@
|
||||
>{{ $t('requestFeature') }}</a>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="topbar-item droppable"
|
||||
v-if="user.permissions.fullAccess ||
|
||||
user.permissions.userSupport"
|
||||
:class="{
|
||||
'active': $route.path.startsWith('/admin')}"
|
||||
>
|
||||
<div
|
||||
class="chevron rotate"
|
||||
@click="dropdownMobile($event)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="chevron-icon-down"
|
||||
v-html="icons.chevronDown"
|
||||
></div>
|
||||
</div>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
{{ $t('admin') }}
|
||||
</router-link>
|
||||
<div class="topbar-dropdown">
|
||||
<router-link
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
{{ $t("adminPanel") }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'blockers'}"
|
||||
>
|
||||
{{ $t("siteBlockers") }}
|
||||
</router-link>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
target="_blank"
|
||||
href="https://panel.habitica.com"
|
||||
>
|
||||
{{ $t('newsroom') }}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</b-navbar-nav>
|
||||
<div class="currency-tray form-inline">
|
||||
<div
|
||||
|
||||
@@ -19,16 +19,12 @@ const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/i
|
||||
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
|
||||
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
|
||||
|
||||
// Admin Panel
|
||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel');
|
||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/search');
|
||||
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/blocker');
|
||||
// Except for tasks that are always loaded all the other main level
|
||||
// All the main level
|
||||
// components are loaded in separate webpack chunks.
|
||||
// See https://webpack.js.org/guides/code-splitting-async/
|
||||
// for docs
|
||||
// Admin Pages
|
||||
const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/container');
|
||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
|
||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
|
||||
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
|
||||
|
||||
// Tasks
|
||||
const UserTasks = () => import(/* webpackChunkName: "userTasks" */'@/components/tasks/user');
|
||||
@@ -185,8 +181,8 @@ const router = new VueRouter({
|
||||
|
||||
{
|
||||
name: 'adminPanel',
|
||||
path: '/admin-panel',
|
||||
component: AdminPanelPage,
|
||||
path: '/admin',
|
||||
component: AdminContainerPage,
|
||||
meta: {
|
||||
privilegeNeeded: [ // any one of these is enough to give access
|
||||
'userSupport',
|
||||
@@ -194,38 +190,51 @@ const router = new VueRouter({
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'adminPanelSearch',
|
||||
path: 'search/:userIdentifier',
|
||||
component: AdminPanelSearchPage,
|
||||
name: 'adminPanel',
|
||||
path: 'panel',
|
||||
component: AdminPanelPage,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
privilegeNeeded: [ // any one of these is enough to give access
|
||||
'userSupport',
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'adminPanelSearch',
|
||||
path: 'search/:userIdentifier',
|
||||
component: AdminPanelSearchPage,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
'userSupport',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'adminPanelUser',
|
||||
path: ':userIdentifier',
|
||||
component: AdminPanelUserPage,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
'userSupport',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'adminPanelUser',
|
||||
path: ':userIdentifier',
|
||||
component: AdminPanelUserPage,
|
||||
name: 'blockers',
|
||||
path: 'blockers',
|
||||
component: BlockerPage,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
privilegeNeeded: [ // any one of these is enough to give access
|
||||
'userSupport',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blockers',
|
||||
path: '/blockers',
|
||||
component: BlockerPage,
|
||||
meta: {
|
||||
privilegeNeeded: [ // any one of these is enough to give access
|
||||
'userSupport',
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
// Only used to handle some redirects
|
||||
// See router.beforeEach
|
||||
{ path: '/static/tavern-and-guilds', redirect: '/static/faq/tavern-and-guilds' },
|
||||
|
||||
5
website/common/locales/en/admin.json
Normal file
5
website/common/locales/en/admin.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"adminPanel": "Admin Panel",
|
||||
"siteBlockers": "Site Blockers",
|
||||
"newsroom": "Newsroom"
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export default {
|
||||
noNewsPosterAccess: 'You don\'t have news poster access.',
|
||||
|
||||
ipAddressBlocked: 'Your access to Habitica has been blocked. This may be due to a breach of our Terms of Service or for other reasons. For details or to ask to be unblocked, please email admin@habitica.com or ask your parent or guardian to email them. Include your Habitica @Username or User Id in the email if you know it.',
|
||||
clientBlocked: 'This clients access to Habitica has been blocked. This may be due to a breach of our Terms of Service or for other reasons. For details or to ask to be unblocked, please email admin@habitica.com or ask your parent or guardian to email them. Include your Habitica @Username or User Id in the email if you know it.',
|
||||
clientRateLimited: 'This User ID or IP address has been rate limited due to an excess amount of requests to the Habitica API v3. More info can be found in the response headers and at https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines#rate-limiting .',
|
||||
|
||||
invalidPlatform: 'Invalid platform specified',
|
||||
|
||||
@@ -23,35 +23,43 @@ const blockedIps = BLOCKED_IPS_RAW
|
||||
.filter(blockedIp => Boolean(blockedIp))
|
||||
: [];
|
||||
|
||||
const blockedClients = [];
|
||||
|
||||
Blocker.watchBlockers({
|
||||
type: 'ipaddress',
|
||||
$or: [
|
||||
{ type: 'ipaddress' },
|
||||
{ type: 'client' },
|
||||
],
|
||||
area: 'full',
|
||||
}, {
|
||||
initial: true,
|
||||
}).on('change', async change => {
|
||||
const { operation, blocker } = change;
|
||||
const checkedList = blocker.type === 'ipaddress' ? blockedIps : blockedClients;
|
||||
if (operation === 'add') {
|
||||
if (blocker.value && !blockedIps.includes(blocker.value)) {
|
||||
blockedIps.push(blocker.value);
|
||||
if (blocker.value && !checkedList.includes(blocker.value)) {
|
||||
checkedList.push(blocker.value);
|
||||
}
|
||||
} else if (operation === 'delete') {
|
||||
const index = blockedIps.indexOf(blocker.value);
|
||||
const index = checkedList.indexOf(blocker.value);
|
||||
if (index !== -1) {
|
||||
blockedIps.splice(index, 1);
|
||||
checkedList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default function ipBlocker (req, res, next) {
|
||||
// If there are no IPs to block, skip the middleware
|
||||
if (blockedIps.length === 0) return next();
|
||||
// Is the client IP, req.ip, blocked?
|
||||
const match = blockedIps.find(blockedIp => blockedIp === req.ip) !== undefined;
|
||||
if (blockedIps.length === 0 && blockedClients.length === 0) return next();
|
||||
|
||||
if (match === true) {
|
||||
// Not translated because no user is loaded at this point
|
||||
const ipMatch = blockedIps.find(blockedIp => blockedIp === req.ip) !== undefined;
|
||||
if (ipMatch === true) {
|
||||
return next(new Forbidden(apiError('ipAddressBlocked')));
|
||||
}
|
||||
|
||||
const clientMatch = blockedClients.find(blockedClient => blockedClient === req.headers['x-client']) !== undefined;
|
||||
if (clientMatch === true) {
|
||||
return next(new Forbidden(apiError('clientBlocked')));
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
forceSSL,
|
||||
forceHabitica,
|
||||
} from './redirects';
|
||||
import ipBlocker from './ipBlocker';
|
||||
import blocker from './blocker';
|
||||
import v1 from './v1';
|
||||
import v2 from './v2';
|
||||
import appRoutes from './appRoutes';
|
||||
@@ -81,7 +81,7 @@ export default function attachMiddlewares (app, server) {
|
||||
|
||||
app.use(maintenanceMode);
|
||||
|
||||
app.use(ipBlocker);
|
||||
app.use(blocker);
|
||||
|
||||
app.use(cors);
|
||||
app.use(forceSSL);
|
||||
|
||||
@@ -7,6 +7,7 @@ import baseModel from '../libs/baseModel';
|
||||
export const blockTypes = [
|
||||
'ipaddress',
|
||||
'email',
|
||||
'client',
|
||||
];
|
||||
|
||||
export const blockArea = [
|
||||
|
||||
Reference in New Issue
Block a user