restructure admin pages

This commit is contained in:
Phillip Thelen
2025-06-18 14:24:38 +02:00
parent 2a2bea07ab
commit e9b2c1b51a
36 changed files with 246 additions and 218 deletions

View File

@@ -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: {

View File

@@ -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 = [
{

View File

@@ -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: {

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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: {

View File

@@ -180,7 +180,7 @@
<script>
import moment from 'moment';
import { userStateMixin } from '../../../mixins/userState';
import { userStateMixin } from '../../../../mixins/userState';
export default {
filters: {

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}

View 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>

View File

@@ -286,7 +286,7 @@
:to="{ name: 'adminPanelUser',
params: { userIdentifier: hero._id } }"
>
admin panel
{{ $t("adminPanel") }}
</router-link>
</span>
</td>

View File

@@ -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

View File

@@ -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' },

View File

@@ -0,0 +1,5 @@
{
"adminPanel": "Admin Panel",
"siteBlockers": "Site Blockers",
"newsroom": "Newsroom"
}

View File

@@ -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',

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -7,6 +7,7 @@ import baseModel from '../libs/baseModel';
export const blockTypes = [
'ipaddress',
'email',
'client',
];
export const blockArea = [