Migrate the Administration panel to its own Mini-SPA.

This commit is contained in:
Buster Neece 2023-08-08 12:23:01 -05:00
parent b502406766
commit 631dda96d8
No known key found for this signature in database
64 changed files with 741 additions and 724 deletions

View File

@ -10,7 +10,6 @@ return static function (App $app) {
$app->group(
'',
function (RouteCollectorProxy $group) {
call_user_func(include(__DIR__ . '/routes/admin.php'), $group);
call_user_func(include(__DIR__ . '/routes/base.php'), $group);
call_user_func(include(__DIR__ . '/routes/public.php'), $group);
call_user_func(include(__DIR__ . '/routes/stations.php'), $group);

View File

@ -1,92 +0,0 @@
<?php
declare(strict_types=1);
use App\Controller;
use App\Enums\GlobalPermissions;
use App\Middleware;
use Slim\Routing\RouteCollectorProxy;
return static function (RouteCollectorProxy $app) {
$app->group(
'/admin',
function (RouteCollectorProxy $group) {
$group->get('', Controller\Admin\IndexAction::class)
->setName('admin:index:index');
$group->get('/debug', Controller\Admin\DebugAction::class)
->setName('admin:debug:index');
$group->group(
'/install',
function (RouteCollectorProxy $group) {
$group->get('/shoutcast', Controller\Admin\ShoutcastAction::class)
->setName('admin:install_shoutcast:index');
$group->get('/stereo_tool', Controller\Admin\StereoToolAction::class)
->setName('admin:install_stereo_tool:index');
$group->get('/geolite', Controller\Admin\GeoLiteAction::class)
->setName('admin:install_geolite:index');
}
)->add(new Middleware\Permissions(GlobalPermissions::Settings));
$group->get('/auditlog', Controller\Admin\AuditLogAction::class)
->setName('admin:auditlog:index')
->add(new Middleware\Permissions(GlobalPermissions::Logs));
$group->get('/api-keys', Controller\Admin\ApiKeysAction::class)
->setName('admin:api:index')
->add(new Middleware\Permissions(GlobalPermissions::ApiKeys));
$group->get('/backups', Controller\Admin\BackupsAction::class)
->setName('admin:backups:index')
->add(new Middleware\Permissions(GlobalPermissions::Backups));
$group->get('/branding', Controller\Admin\BrandingAction::class)
->setName('admin:branding:index')
->add(new Middleware\Permissions(GlobalPermissions::Settings));
$group->get('/custom_fields', Controller\Admin\CustomFieldsAction::class)
->setName('admin:custom_fields:index')
->add(new Middleware\Permissions(GlobalPermissions::CustomFields));
$group->get('/logs', Controller\Admin\LogsAction::class)
->setName('admin:logs:index')
->add(new Middleware\Permissions(GlobalPermissions::Logs));
$group->get('/permissions', Controller\Admin\PermissionsAction::class)
->setName('admin:permissions:index')
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/relays', Controller\Admin\RelaysAction::class)
->setName('admin:relays:index')
->add(new Middleware\Permissions(GlobalPermissions::Stations));
$group->map(['GET', 'POST'], '/settings', Controller\Admin\SettingsAction::class)
->setName('admin:settings:index')
->add(new Middleware\Permissions(GlobalPermissions::Settings));
$group->get('/stations', Controller\Admin\StationsAction::class)
->setName('admin:stations:index')
->add(new Middleware\Permissions(GlobalPermissions::Stations));
$group->get('/storage_locations', Controller\Admin\StorageLocationsAction::class)
->setName('admin:storage_locations:index')
->add(new Middleware\Permissions(GlobalPermissions::StorageLocations));
$group->get('/updates', Controller\Admin\UpdatesAction::class)
->setName('admin:updates:index')
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/users', Controller\Admin\UsersAction::class)
->setName('admin:users:index')
->add(new Middleware\Permissions(GlobalPermissions::All));
}
)
->add(Middleware\Module\Admin::class)
->add(Middleware\Module\PanelLayout::class)
->add(Middleware\EnableView::class)
->add(new Middleware\Permissions(GlobalPermissions::View))
->add(Middleware\RequireLogin::class);
};

View File

@ -95,6 +95,7 @@ return static function (RouteCollectorProxy $app) {
call_user_func(include(__DIR__ . '/api_admin.php'), $group);
call_user_func(include(__DIR__ . '/api_frontend.php'), $group);
call_user_func(include(__DIR__ . '/api_station.php'), $group);
call_user_func(include(__DIR__ . '/api_vue.php'), $group);
}
)->add(Middleware\Module\Api::class);
};

52
config/routes/api_vue.php Normal file
View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
use App\Controller;
use App\Enums\GlobalPermissions;
use App\Middleware;
use Slim\Routing\RouteCollectorProxy;
return static function (RouteCollectorProxy $app) {
$app->group(
'/vue/admin',
function (RouteCollectorProxy $group) {
$group->get('/backups', Controller\Api\VueProps\Admin\BackupsAction::class)
->setName('api:vue:admin:backups')
->add(new Middleware\Permissions(GlobalPermissions::Backups));
$group->get('/custom_fields', Controller\Api\VueProps\Admin\CustomFieldsAction::class)
->setName('api:vue:admin:custom_fields')
->add(new Middleware\Permissions(GlobalPermissions::CustomFields));
$group->get('/debug', Controller\Api\VueProps\Admin\DebugAction::class)
->setName('api:vue:admin:debug')
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/logs', Controller\Api\VueProps\Admin\LogsAction::class)
->setName('api:vue:admin:logs')
->add(new Middleware\Permissions(GlobalPermissions::Logs));
$group->get('/permissions', Controller\Api\VueProps\Admin\PermissionsAction::class)
->setName('api:vue:admin:permissions')
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/settings', Controller\Api\VueProps\Admin\SettingsAction::class)
->setName('api:vue:admin:settings')
->add(new Middleware\Permissions(GlobalPermissions::Settings));
$group->get('/stations', Controller\Api\VueProps\Admin\StationsAction::class)
->setName('api:vue:admin:stations')
->add(new Middleware\Permissions(GlobalPermissions::Stations));
$group->get('/updates', Controller\Api\VueProps\Admin\UpdatesAction::class)
->setName('api:vue:admin:updates')
->add(new Middleware\Permissions(GlobalPermissions::All));
$group->get('/users', Controller\Api\VueProps\Admin\UsersAction::class)
->setName('api:vue:admin:users')
->add(new Middleware\Permissions(GlobalPermissions::All));
}
)->add(new Middleware\Permissions(GlobalPermissions::View))
->add(Middleware\RequireLogin::class);
};

View File

@ -24,7 +24,6 @@ return static function (RouteCollectorProxy $app) {
->setName('account:endmasquerade')
->add(Middleware\RequireLogin::class);
$app->group(
'',
function (RouteCollectorProxy $group) {
@ -75,4 +74,11 @@ return static function (RouteCollectorProxy $app) {
->add(Middleware\Module\PanelLayout::class);
}
)->add(Middleware\EnableView::class);
$app->get('/admin', Controller\Admin\IndexAction::class)
->setName('admin:index:index')
->add(Middleware\Module\PanelLayout::class)
->add(Middleware\EnableView::class)
->add(new Middleware\Permissions(GlobalPermissions::View))
->add(Middleware\RequireLogin::class);
};

View File

@ -41,12 +41,14 @@
"vue-axios": "^3.5",
"vue-codemirror6": "^1.0",
"vue-easy-lightbox": "^1.16",
"vue-router": "^4.2.4",
"vue3-gettext": "^2.3.4",
"vuedraggable": "^4.1.0",
"wavesurfer.js": "^7",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@types/lodash": "^4.14.196",
"@types/luxon": "^3.3.1",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
@ -949,6 +951,12 @@
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.196",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz",
"integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==",
"dev": true
},
"node_modules/@types/luxon": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.1.tgz",
@ -4519,6 +4527,20 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/vue-router": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.4.tgz",
"integrity": "sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==",
"dependencies": {
"@vue/devtools-api": "^6.5.0"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue3-gettext": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/vue3-gettext/-/vue3-gettext-2.4.0.tgz",

View File

@ -41,12 +41,14 @@
"vue-axios": "^3.5",
"vue-codemirror6": "^1.0",
"vue-easy-lightbox": "^1.16",
"vue-router": "^4.2.4",
"vue3-gettext": "^2.3.4",
"vuedraggable": "^4.1.0",
"wavesurfer.js": "^7",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@types/lodash": "^4.14.196",
"@types/luxon": "^3.3.1",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",

View File

@ -4,14 +4,15 @@
}
@include media-breakpoint-up(lg) {
body.has-sidebar {
#main {
margin-left: $navdrawer-width;
}
#main.has-sidebar {
margin-left: $navdrawer-width;
}
footer.has-sidebar {
left: $navdrawer-width;
}
#footer {
left: $navdrawer-width;
}
.offcanvas-backdrop {
display: none;
}
.offcanvas.navdrawer {

View File

@ -0,0 +1,30 @@
<template>
<panel-layout v-bind="panelProps">
<template
v-if="!isHome"
#sidebar
>
<sidebar v-bind="sidebarProps" />
</template>
<template #default>
<router-view />
</template>
</panel-layout>
</template>
<script setup lang="ts">
import PanelLayout from "~/components/PanelLayout.vue";
import {useAzuraCast} from "~/vendor/azuracast.ts";
import {useRoute} from "vue-router";
import {ref, watch} from "vue";
import Sidebar from "~/components/Admin/Sidebar.vue";
const {panelProps, sidebarProps} = useAzuraCast();
const isHome = ref(true);
const route = useRoute();
watch(route, (newRoute) => {
isHome.value = newRoute.name === 'admin:index';
});
</script>

View File

@ -195,6 +195,7 @@ const settings = ref({...blankSettings});
const {$gettext} = useTranslate();
const {timeConfig} = useAzuraCast();
const {DateTime} = useLuxon();
const fields = [
{

View File

@ -6,8 +6,8 @@
<div class="row row-of-cards">
<div
v-for="(panel, key) in adminPanels"
:key="key"
v-for="panel in menuItems"
:key="panel.key"
class="col-sm-12 col-lg-4"
>
<section class="card">
@ -26,14 +26,14 @@
</div>
<div class="list-group list-group-flush">
<a
v-for="(item, itemKey) in panel.items"
:key="itemKey"
:href="item.url"
<router-link
v-for="item in panel.items"
:key="item.key"
:to="item.url"
class="list-group-item list-group-item-action"
>
{{ item.label }}
</a>
</router-link>
</div>
</section>
</div>
@ -415,14 +415,13 @@ import RunningBadge from "~/components/Common/Badges/RunningBadge.vue";
import {onMounted, ref, shallowRef} from "vue";
import {useAxios} from "~/vendor/axios";
import {useNotify} from "~/functions/useNotify";
import {useAzuraCast} from "~/vendor/azuracast";
import {getApiUrl} from "~/router";
import {useAdminMenu} from "~/components/Admin/menu";
const statsUrl = getApiUrl('/admin/server/stats');
const servicesUrl = getApiUrl('/admin/services');
const {sidebarProps} = useAzuraCast();
const adminPanels = sidebarProps.menu;
const menuItems = useAdminMenu();
const stats = shallowRef({
cpu: {

View File

@ -1,34 +1,19 @@
<template>
<div class="navdrawer-header">
<a
<router-link
:to="{ name: 'admin:index'}"
class="navbar-brand px-0"
:href="homeUrl"
>
{{ $gettext('Administration') }}
</a>
</router-link>
</div>
<sidebar-menu
:menu="menu"
:active="active"
/>
<sidebar-menu :menu="menuItems" />
</template>
<script setup>
import SidebarMenu from "~/components/Common/SidebarMenu.vue";
import SidebarMenu from "~/components/Common/SidebarMenuNew.vue";
import {useAdminMenu} from "~/components/Admin/menu";
const props = defineProps({
homeUrl: {
type: String,
required: true
},
menu: {
type: Object,
required: true
},
active: {
type: String,
default: null
}
});
const menuItems = useAdminMenu();
</script>

View File

@ -101,16 +101,15 @@
v-if="enableWebUpdates"
#footer_actions
>
<a
<router-link
:to="{ name: 'admin:backups:index' }"
class="btn btn-dark"
:href="backupUrl"
target="_blank"
>
<icon icon="backup" />
<span>
{{ $gettext('Backup') }}
</span>
</a>
</router-link>
<button
type="button"
class="btn btn-success"
@ -172,10 +171,6 @@ const props = defineProps({
return {};
}
},
backupUrl: {
type: String,
required: true
},
updatesApiUrl: {
type: String,
required: true

View File

@ -0,0 +1,169 @@
import {useTranslate} from "~/vendor/gettext.ts";
import {GlobalPermission, userAllowed} from "~/acl.ts";
import filterMenu from "~/functions/filterMenu.ts";
export function useAdminMenu(): array {
const {$gettext} = useTranslate();
const menu = [
{
key: 'maintenance',
label: $gettext('System Maintenance'),
icon: 'router',
items: [
{
key: 'settings',
label: $gettext('System Settings'),
url: {
name: 'admin:settings:index'
},
visible: userAllowed(GlobalPermission.Settings)
},
{
key: 'branding',
label: $gettext('Custom Branding'),
url: {
name: 'admin:branding:index'
},
visible: userAllowed(GlobalPermission.Settings)
},
{
key: 'logs',
label: $gettext('System Logs'),
url: {
name: 'admin:logs:index'
},
visible: userAllowed(GlobalPermission.Logs)
},
{
key: 'storage_locations',
label: $gettext('Storage Locations'),
url: {
name: 'admin:storage_locations:index'
},
visible: userAllowed(GlobalPermission.StorageLocations)
},
{
key: 'backups',
label: $gettext('Backups'),
url: {
name: 'admin:backups:index'
},
visible: userAllowed(GlobalPermission.Backups)
},
{
key: 'debug',
label: $gettext('System Debugger'),
url: {
name: 'admin:debug:index'
},
visible: userAllowed(GlobalPermission.All)
},
{
key: 'updates',
label: $gettext('Update AzuraCast'),
url: {
name: 'admin:updates:index'
},
visible: userAllowed(GlobalPermission.All)
}
]
},
{
key: 'users',
label: $gettext('Users'),
icon: 'group',
items: [
{
key: 'manage_users',
label: $gettext('User Accounts'),
url: {
name: 'admin:users:index'
},
visible: userAllowed(GlobalPermission.All)
},
{
key: 'permissions',
label: $gettext('Roles & Permissions'),
url: {
name: 'admin:permissions:index'
},
visible: userAllowed(GlobalPermission.All)
},
{
key: 'auditlog',
label: $gettext('Audit Log'),
url: {
name: 'admin:auditlog:index'
},
visible: userAllowed(GlobalPermission.Logs)
},
{
key: 'api_keys',
label: $gettext('API Keys'),
url: {
name: 'admin:api:index'
},
visible: userAllowed(GlobalPermission.ApiKeys)
}
]
},
{
key: 'stations',
label: $gettext('Stations'),
icon: 'volume_up',
items: [
{
key: 'manage_stations',
label: $gettext('Stations'),
url: {
name: 'admin:stations:index'
},
visible: userAllowed(GlobalPermission.Stations)
},
{
key: 'custom_fields',
label: $gettext('Custom Fields'),
url: {
name: 'admin:custom_fields:index'
},
visible: userAllowed(GlobalPermission.CustomFields)
},
{
key: 'relays',
label: $gettext('Connected AzuraRelays'),
url: {
name: 'admin:relays:index',
},
visible: userAllowed(GlobalPermission.Stations)
},
{
key: 'shoutcast',
label: $gettext('Install Shoutcast'),
url: {
name: 'admin:install_shoutcast:index'
},
visible: userAllowed(GlobalPermission.All)
},
{
key: 'stereo_tool',
label: $gettext('Install Stereo Tool'),
url: {
name: 'admin:stereo_tool:index'
},
visible: userAllowed(GlobalPermission.All)
},
{
key: 'geolite',
label: $gettext('Install GeoLite IP Database'),
url: {
name: 'admin:install_geolite:index'
},
visible: userAllowed(GlobalPermission.All)
}
]
}
];
return filterMenu(menu);
}

View File

@ -0,0 +1,134 @@
<template>
<ul class="offcanvas-body navdrawer-nav">
<li
v-for="category in menu"
:key="category.key"
class="nav-item"
>
<router-link
v-if="isRouteLink(category)"
:class="getLinkClass(category)"
:to="category.url"
class="nav-link"
>
<icon
class="navdrawer-nav-icon"
:icon="category.icon"
/>
{{ category.label }}
</router-link>
<a
v-bind="getCategoryLink(category)"
class="nav-link"
:class="getLinkClass(category)"
>
<icon
class="navdrawer-nav-icon"
:icon="category.icon"
/>
{{ category.label }}
<icon
v-if="category.external"
class="sm ms-2"
icon="open_in_new"
:aria-label="$gettext('External')"
/>
</a>
<div
v-if="category.items"
:id="'sidebar-submenu-'+category.key"
class="collapse pb-2"
:class="(isActiveItem(category)) ? 'show' : ''"
>
<ul class="navdrawer-nav">
<li
v-for="item in category.items"
:key="item.key"
class="nav-item"
>
<router-link
v-if="isRouteLink(item)"
:to="item.url"
class="nav-link ps-4 py-2"
:class="getLinkClass(item)"
>
{{ item.label }}
</router-link>
<a
v-else
class="nav-link ps-4 py-2"
:class="item.class"
:href="item.url"
:target="(item.external) ? '_blank' : ''"
:title="item.title"
>
{{ item.label }}
<icon
v-if="item.external"
class="sm ms-2"
icon="open_in_new"
:aria-label="$gettext('External')"
/>
</a>
</li>
</ul>
</div>
</li>
</ul>
</template>
<script setup lang="ts">
import Icon from "~/components/Common/Icon.vue";
import {useRoute} from "vue-router";
import {some} from "lodash";
const props = defineProps({
menu: {
type: Object,
required: true
},
});
const currentRoute = useRoute();
const isRouteLink = (item) => {
return (typeof (item.url) !== 'undefined')
&& (typeof (item.url) !== 'string');
};
const isActiveItem = (item) => {
if (item.items && some(item.items, isActiveItem)) {
return true;
}
return isRouteLink(item) && item.url.name === currentRoute.name;
};
const getLinkClass = (item) => {
return [
item.class ?? null,
isActiveItem(item) ? 'active' : ''
];
}
const getCategoryLink = (item) => {
const linkAttrs = {};
if (item.items) {
linkAttrs['data-bs-toggle'] = 'collapse';
linkAttrs.href = '#sidebar-submenu-' + item.key;
} else {
linkAttrs.href = category.url;
}
if (item.external) {
linkAttrs.target = '_blank';
}
if (item.title) {
linkAttrs.title = category.title;
}
return linkAttrs;
}
</script>

View File

@ -132,11 +132,11 @@
<slot name="sidebar" />
</nav>
<section id="main">
<main
id="content"
:class="[(slots.sidebar) ? 'content-alt' : '']"
>
<section
id="main"
:class="[(slots.sidebar) ? 'has-sidebar' : '']"
>
<main id="content">
<div class="container">
<slot />
</div>
@ -145,7 +145,7 @@
<footer
id="footer"
:class="[(slots.sidebar) ? 'footer-alt' : '']"
:class="[(slots.sidebar) ? 'has-sidebar' : '']"
>
{{ $gettext('Powered by') }}
<a
@ -167,7 +167,7 @@
</template>
<script setup>
import {onMounted, onUnmounted, useSlots} from "vue";
import {useSlots, watch} from "vue";
import Icon from "~/components/Common/Icon.vue";
import {switchTheme} from "!/js/layout";
@ -205,20 +205,26 @@ const props = defineProps({
required: true
},
platform: {
type: String,
required: true
type: String,
required: true
}
});
const slots = useSlots();
onMounted(() => {
const handleSidebar = () => {
if (slots.sidebar) {
document.body.classList.add('has-sidebar');
} else {
document.body.classList.remove('has-sidebar');
}
});
}
onUnmounted(() => {
document.body.classList.remove('has-sidebar');
});
watch(
() => slots.sidebar,
handleSidebar,
{
immediate: true
}
);
</script>

View File

@ -0,0 +1,19 @@
<template>
<async-comp v-bind="state" />
</template>
<script setup>
import {defineAsyncComponent} from "vue";
const props = defineProps({
component: {
type: Function,
required: true
},
state: {
type: Object,
required: true
}
});
const AsyncComp = defineAsyncComponent(props.component);
</script>

View File

@ -0,0 +1,32 @@
import {cloneDeep, filter, forEach, get} from "lodash";
export default function filterMenu(menuItems) {
const newMenu = [];
forEach(cloneDeep(menuItems), (menuRow) => {
const itemIsVisible: boolean = get(menuRow, 'visible', true);
if (!itemIsVisible) {
return;
}
const newMenuRow = {
...menuRow
};
if ('items' in menuRow) {
const newMenuRowItems = filter(menuRow.items, (item) => {
return get(item, 'visible', true);
});
if (newMenuRowItems.length === 0) {
return;
}
newMenuRow.items = newMenuRowItems;
}
newMenu.push(newMenuRow);
});
return newMenu;
}

142
frontend/vue/pages/Admin.js Normal file
View File

@ -0,0 +1,142 @@
import initApp from "~/layout";
import {h, toValue} from "vue";
import {createRouter, createWebHashHistory} from "vue-router";
import AdminLayout from "~/components/Admin/AdminLayout.vue";
import {getApiUrl} from "~/router";
import axios from "axios";
const {vueApp} = initApp({
render() {
return h(AdminLayout);
}
});
const populateComponentRemotely = (url) => {
return {
beforeEnter: (to, from, next) => {
axios.get(toValue(url)).then((resp) => {
Object.assign(to.meta, {
state: resp.data
});
next();
});
},
props: (route) => ({
...route.meta.state
})
}
}
const routes = [
{
path: '/',
component: () => import('~/components/Admin/Index.vue'),
name: 'admin:index'
},
{
path: '/api-keys',
component: () => import('~/components/Admin/ApiKeys.vue'),
name: 'admin:api-keys:index'
},
{
path: '/settings',
name: 'admin:settings:index',
component: () => import('~/components/Admin/Settings.vue'),
...populateComponentRemotely(getApiUrl('/vue/admin/settings'))
},
{
path: '/branding',
component: () => import('~/components/Admin/Branding.vue'),
name: 'admin:branding:index'
},
{
path: '/logs',
name: 'admin:logs:index',
component: () => import('~/components/Admin/Logs.vue'),
...populateComponentRemotely(getApiUrl('/vue/admin/logs'))
},
{
path: '/storage_locations',
component: () => import('~/components/Admin/StorageLocations.vue'),
name: 'admin:storage_locations:index'
},
{
path: '/backups',
component: () => import('~/components/Admin/Backups.vue'),
name: 'admin:backups:index',
...populateComponentRemotely(getApiUrl('/vue/admin/backups'))
},
{
path: '/debug',
component: () => import('~/components/Admin/Debug.vue'),
name: 'admin:debug:index',
...populateComponentRemotely(getApiUrl('/vue/admin/debug'))
},
{
path: '/updates',
component: () => import('~/components/Admin/Updates.vue'),
name: 'admin:updates:index',
...populateComponentRemotely(getApiUrl('/vue/admin/updates'))
},
{
path: '/users',
component: () => import('~/components/Admin/Users.vue'),
name: 'admin:users:index',
...populateComponentRemotely(getApiUrl('/vue/admin/users'))
},
{
path: '/permissions',
component: () => import('~/components/Admin/Permissions.vue'),
name: 'admin:permissions:index',
...populateComponentRemotely(getApiUrl('/vue/admin/permissions'))
},
{
path: '/auditlog',
component: () => import('~/components/Admin/AuditLog.vue'),
name: 'admin:auditlog:index'
},
{
path: '/api_keys',
component: () => import('~/components/Admin/ApiKeys.vue'),
name: 'admin:api:index'
},
{
path: '/stations',
component: () => import('~/components/Admin/Stations.vue'),
name: 'admin:stations:index',
...populateComponentRemotely(getApiUrl('/vue/admin/stations'))
},
{
path: '/custom_fields',
component: () => import('~/components/Admin/CustomFields.vue'),
name: 'admin:custom_fields:index',
...populateComponentRemotely(getApiUrl('/vue/admin/custom_fields'))
},
{
path: '/relays',
component: () => import('~/components/Admin/Relays.vue'),
name: 'admin:relays:index',
},
{
path: '/install_shoutcast',
component: () => import('~/components/Admin/Shoutcast.vue'),
name: 'admin:install_shoutcast:index'
},
{
path: '/install_stereo_tool',
component: () => import('~/components/Admin/StereoTool.vue'),
name: 'admin:stereo_tool:index'
},
{
path: '/install_geolite',
component: () => import('~/components/Admin/GeoLite.vue'),
name: 'admin:install_geolite:index'
}
]
const router = createRouter({
history: createWebHashHistory(),
routes,
});
vueApp.use(router);

View File

@ -1,5 +0,0 @@
import AdminApiKeys from '~/components/Admin/ApiKeys.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminApiKeys));

View File

@ -1,5 +0,0 @@
import AuditLog from '~/components/Admin/AuditLog.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AuditLog));

View File

@ -1,5 +0,0 @@
import AdminBackups from '~/components/Admin/Backups.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminBackups));

View File

@ -1,5 +0,0 @@
import AdminBranding from '~/components/Admin/Branding.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminBranding));

View File

@ -1,5 +0,0 @@
import AdminCustomFields from '~/components/Admin/CustomFields.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminCustomFields));

View File

@ -1,5 +0,0 @@
import AdminDebug from '~/components/Admin/Debug.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminDebug));

View File

@ -1,5 +0,0 @@
import AdminGeoLite from '~/components/Admin/GeoLite.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminGeoLite));

View File

@ -1,5 +0,0 @@
import AdminIndex from '~/components/Admin/Index.vue';
import initApp from "~/layout";
import usePanelLayout from "~/layouts/PanelLayout";
initApp(usePanelLayout(AdminIndex));

View File

@ -1,5 +0,0 @@
import AdminLogs from '~/components/Admin/Logs.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminLogs));

View File

@ -1,5 +0,0 @@
import AdminPermissions from '~/components/Admin/Permissions.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminPermissions));

View File

@ -1,5 +0,0 @@
import AdminRelays from '~/components/Admin/Relays.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminRelays));

View File

@ -1,5 +0,0 @@
import AdminSettings from '~/components/Admin/Settings.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminSettings));

View File

@ -1,5 +0,0 @@
import AdminShoutcast from '~/components/Admin/Shoutcast.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminShoutcast));

View File

@ -1,5 +0,0 @@
import AdminStations from '~/components/Admin/Stations.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminStations));

View File

@ -1,5 +0,0 @@
import AdminStereoTool from '~/components/Admin/StereoTool.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminStereoTool));

View File

@ -1,5 +0,0 @@
import StorageLocations from '~/components/Admin/StorageLocations.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(StorageLocations));

View File

@ -1,5 +0,0 @@
import AdminUpdates from '~/components/Admin/Updates.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminUpdates));

View File

@ -1,5 +0,0 @@
import AdminUsers from '~/components/Admin/Users.vue';
import initApp from "~/layout";
import useAdminPanelLayout from "~/layouts/AdminPanelLayout";
initApp(useAdminPanelLayout(AdminUsers));

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
final class ApiKeysAction implements SingleActionInterface
{
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/ApiKeys',
id: 'admin-api-keys',
title: __('API Keys'),
);
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
final class AuditLogAction implements SingleActionInterface
{
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/AuditLog',
id: 'admin-audit-log',
title: __('Audit Log'),
);
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
final class BrandingAction implements SingleActionInterface
{
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Branding',
id: 'admin-branding',
title: __('Custom Branding'),
);
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
final class GeoLiteAction implements SingleActionInterface
{
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/GeoLite',
id: 'admin-geolite',
title: __('Install GeoLite IP Database'),
);
}
}

View File

@ -18,7 +18,7 @@ final class IndexAction implements SingleActionInterface
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Index',
component: 'Admin',
id: 'admin-index',
title: __('Administration'),
);

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
final class RelaysAction implements SingleActionInterface
{
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Relays',
id: 'admin-relays',
title: __('Connected AzuraRelays')
);
}
}

View File

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
final class ShoutcastAction implements SingleActionInterface
{
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
if ('x86_64' !== php_uname('m')) {
throw new RuntimeException('Shoutcast cannot be installed on non-X86_64 systems.');
}
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Shoutcast',
id: 'admin-shoutcast',
title: __('Install Shoutcast 2 DNAS'),
);
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
final class StereoToolAction implements SingleActionInterface
{
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/StereoTool',
id: 'admin-stereo-tool',
title: __('Install Stereo Tool'),
);
}
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
final class StorageLocationsAction implements SingleActionInterface
{
public function __invoke(
ServerRequest $request,
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/StorageLocations',
id: 'admin-storage-locations',
title: __('Storage Locations'),
);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Container\EnvironmentAwareTrait;
use App\Controller\SingleActionInterface;
@ -26,17 +26,11 @@ final class BackupsAction implements SingleActionInterface
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Backups',
id: 'admin-backups',
title: __('Backups'),
props: [
'isDocker' => $this->environment->isDocker(),
'storageLocations' => $this->storageLocationRepo->fetchSelectByType(
StorageLocationTypes::Backup
),
],
);
return $response->withJson([
'isDocker' => $this->environment->isDocker(),
'storageLocations' => $this->storageLocationRepo->fetchSelectByType(
StorageLocationTypes::Backup
),
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
@ -17,14 +17,8 @@ final class CustomFieldsAction implements SingleActionInterface
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/CustomFields',
id: 'admin-custom-fields',
title: __('Custom Fields'),
props: [
'autoAssignTypes' => MetadataTags::getNames(),
]
);
return $response->withJson([
'autoAssignTypes' => MetadataTags::getNames(),
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Cache\DatabaseCache;
use App\Console\Command\Sync\SingleTaskCommand;
@ -91,18 +91,12 @@ final class DebugAction implements SingleActionInterface
];
}
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Debug',
id: 'admin-debug',
title: __('System Debugger'),
props: [
'clearCacheUrl' => $router->named('api:admin:debug:clear-cache'),
'clearQueuesUrl' => $router->named('api:admin:debug:clear-queue'),
'syncTasks' => $syncTasks,
'queueTotals' => $queueTotals,
'stations' => $stations,
]
);
return $response->withJson([
'clearCacheUrl' => $router->named('api:admin:debug:clear-cache'),
'clearQueuesUrl' => $router->named('api:admin:debug:clear-queue'),
'syncTasks' => $syncTasks,
'queueTotals' => $queueTotals,
'stations' => $stations,
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Controller\SingleActionInterface;
use App\Entity\Repository\StationRepository;
@ -39,15 +39,9 @@ final class LogsAction implements SingleActionInterface
}
}
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Logs',
id: 'admin-logs',
title: __('System Logs'),
props: [
'systemLogsUrl' => $router->fromHere('api:admin:logs'),
'stationLogs' => $stationLogs,
],
);
return $response->withJson([
'systemLogsUrl' => $router->fromHere('api:admin:logs'),
'stationLogs' => $stationLogs,
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Controller\SingleActionInterface;
use App\Entity\Repository\StationRepository;
@ -26,17 +26,11 @@ final class PermissionsAction implements SingleActionInterface
$actions = $request->getAcl()->listPermissions();
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Permissions',
id: 'admin-permissions',
title: __('Roles & Permissions'),
props: [
'listUrl' => $router->fromHere('api:admin:roles'),
'stations' => $this->stationRepo->fetchSelect(),
'globalPermissions' => $actions['global'],
'stationPermissions' => $actions['station'],
]
);
return $response->withJson([
'listUrl' => $router->fromHere('api:admin:roles'),
'stations' => $this->stationRepo->fetchSelect(),
'globalPermissions' => $actions['global'],
'stationPermissions' => $actions['station'],
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
@ -22,12 +22,6 @@ final class SettingsAction implements SingleActionInterface
Response $response,
array $params
): ResponseInterface {
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Settings',
id: 'admin-settings',
title: __('System Settings'),
props: $this->settingsComponent->getProps($request),
);
return $response->withJson($this->settingsComponent->getProps($request));
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Controller\SingleActionInterface;
use App\Http\Response;
@ -26,12 +26,8 @@ final class StationsAction implements SingleActionInterface
): ResponseInterface {
$router = $request->getRouter();
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Stations',
id: 'admin-stations',
title: __('Stations'),
props: array_merge(
return $response->withJson(
array_merge(
$this->stationFormComponent->getProps($request),
[
'listUrl' => $router->fromHere('api:admin:stations'),

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Container\EnvironmentAwareTrait;
use App\Container\SettingsAwareTrait;
@ -31,18 +31,11 @@ final class UpdatesAction implements SingleActionInterface
$router = $request->getRouter();
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Updates',
id: 'admin-updates',
title: __('Update AzuraCast'),
props: [
'releaseChannel' => $this->version->getReleaseChannelEnum()->value,
'initialUpdateInfo' => $settings->getUpdateResults(),
'backupUrl' => $router->named('admin:backups:index'),
'updatesApiUrl' => $router->named('api:admin:updates'),
'enableWebUpdates' => $this->environment->enableWebUpdater(),
],
);
return $response->withJson([
'releaseChannel' => $this->version->getReleaseChannelEnum()->value,
'initialUpdateInfo' => $settings->getUpdateResults(),
'updatesApiUrl' => $router->named('api:admin:updates'),
'enableWebUpdates' => $this->environment->enableWebUpdater(),
]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace App\Controller\Admin;
namespace App\Controller\Api\VueProps\Admin;
use App\Controller\SingleActionInterface;
use App\Entity\Repository\RoleRepository;
@ -24,15 +24,9 @@ final class UsersAction implements SingleActionInterface
): ResponseInterface {
$router = $request->getRouter();
return $request->getView()->renderVuePage(
response: $response,
component: 'Admin/Users',
id: 'admin-users',
title: __('Users'),
props: [
'listUrl' => $router->fromHere('api:admin:users'),
'roles' => $this->roleRepo->fetchSelect(),
]
);
return $response->withJson([
'listUrl' => $router->fromHere('api:admin:users'),
'roles' => $this->roleRepo->fetchSelect(),
]);
}
}

View File

@ -19,6 +19,8 @@ final class EndMasqueradeAction implements SingleActionInterface
$auth = $request->getAuth();
$auth->endMasquerade();
return $response->withRedirect($request->getRouter()->named('admin:users:index'));
$router = $request->getRouter();
return $response->withRedirect($router->named('admin:index:index') . '#/users');
}
}

View File

@ -41,7 +41,7 @@ final class DashboardAction implements SingleActionInterface
'notificationsUrl' => $router->named('api:frontend:dashboard:notifications'),
'showCharts' => $showCharts,
'chartsUrl' => $router->named('api:frontend:dashboard:charts'),
'manageStationsUrl' => $router->named('admin:stations:index'),
'manageStationsUrl' => $router->named('admin:index:index') . '#/stations',
'stationsUrl' => $router->named('api:frontend:dashboard:stations'),
'showAlbumArt' => !$settings->getHideAlbumArt(),
]

View File

@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Middleware\Module;
use App\Container\SettingsAwareTrait;
use App\Event;
use App\Http\ServerRequest;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Interfaces\RouteInterface;
use Slim\Routing\RouteContext;
/**
* Module middleware for the /admin pages.
*/
final class Admin
{
use SettingsAwareTrait;
public function __construct(
private readonly EventDispatcherInterface $dispatcher,
) {
}
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$settings = $this->readSettings();
$event = new Event\BuildAdminMenu($request, $settings);
$this->dispatcher->dispatch($event);
$view = $request->getView();
$activeTab = null;
$currentRoute = RouteContext::fromRequest($request)->getRoute();
if ($currentRoute instanceof RouteInterface) {
$routeParts = explode(':', $currentRoute->getName() ?? '');
$activeTab = $routeParts[1];
}
$globalProps = $view->getGlobalProps();
$router = $request->getRouter();
$globalProps->set('sidebarProps', [
'homeUrl' => $router->named('admin:index:index'),
'menu' => $event->getFilteredMenu(),
'active' => $activeTab,
]);
return $handler->handle($request);
}
}

View File

@ -56,7 +56,7 @@ final class BaseUrlCheck
$notification->body = implode(' ', $notificationBodyParts);
$notification->type = FlashLevels::Warning->value;
$notification->actionLabel = __('System Settings');
$notification->actionUrl = $router->named('admin:settings:index');
$notification->actionUrl = $router->named('admin:index:index') . '#/settings';
$event->addNotification($notification);
}

View File

@ -50,7 +50,7 @@ final class RecentBackupCheck
$router = $request->getRouter();
$notification->actionLabel = __('Backups');
$notification->actionUrl = $router->named('admin:backups:index');
$notification->actionUrl = $router->named('admin:index:index') . '#/backups';
$event->addNotification($notification);
}

View File

@ -57,7 +57,7 @@ final class SyncTaskCheck
$router = $request->getRouter();
$notification->actionLabel = __('System Debugger');
$notification->actionUrl = $router->named('admin:debug:index');
$notification->actionUrl = $router->named('admin:index:index') . '#/debug';
// phpcs:enable
$event->addNotification($notification);

View File

@ -42,7 +42,7 @@ final class UpdateCheck
$router = $event->getRequest()->getRouter();
$actionLabel = __('Update AzuraCast');
$actionUrl = $router->named('admin:updates:index');
$actionUrl = $router->named('admin:index:index') . '#/updates';
$releaseChannel = $this->version->getReleaseChannelEnum();

View File

@ -85,7 +85,7 @@ final class View extends Engine
function (string $componentPath) use ($vueComponents, $environment) {
$assetRoot = '/static/vite_dist';
if ($environment->isDevelopment()) {
if ($environment->isDevelopment() || $environment->isTesting()) {
return [
'js' => $assetRoot . '/' . $componentPath,
'css' => [],
@ -157,7 +157,6 @@ final class View extends Engine
'auth' => $request->getAttribute(ServerRequest::ATTR_AUTH),
'acl' => $request->getAttribute(ServerRequest::ATTR_ACL),
'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH),
'user' => $request->getAttribute(ServerRequest::ATTR_USER),
];
$router = $request->getAttribute(ServerRequest::ATTR_ROUTER);
@ -192,13 +191,33 @@ final class View extends Engine
// User profile-specific 24-hour display setting.
$userObj = $request->getAttribute(ServerRequest::ATTR_USER);
$show24Hours = ($userObj instanceof User)
? $userObj->getShow24HourTime()
: null;
$requestData['user'] = $userObj;
$timeConfig = new stdClass();
if (null !== $show24Hours) {
$timeConfig->hour12 = !$show24Hours;
if ($userObj instanceof User) {
$timeConfig->hour12 = !$userObj->getShow24HourTime();
$globalPermissions = [];
$stationPermissions = [];
foreach ($userObj->getRoles() as $role) {
foreach ($role->getPermissions() as $permission) {
$station = $permission->getStation();
if (null !== $station) {
$stationPermissions[$station->getIdRequired()][] = $permission->getActionName();
} else {
$globalPermissions[] = $permission->getActionName();
}
}
}
$this->globalProps->set('user', [
'id' => $userObj->getIdRequired(),
'displayName' => $userObj->getDisplayName(),
'globalPermissions' => $globalPermissions,
'stationPermissions' => $stationPermissions,
]);
}
$this->globalProps->set('timeConfig', $timeConfig);

View File

@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace Functional;
use FunctionalTester;
class Admin_RecordsCest extends CestAbstract
{
/**
* @before setupComplete
* @before login
*/
public function manageUsers(FunctionalTester $I): void
{
$I->wantTo('Manage users.');
// User homepage
$I->amOnPage('/admin/users');
$I->seeResponseCodeIs(200);
/*
* TODO: Acceptance Testing with Vue Rendering
$I->see($this->login_username);
// Edit existing user
$I->click('Edit');
$I->submitForm('.form', []);
$I->seeCurrentUrlEquals('/admin/users');
$I->see($this->login_username);
// Add a secondary user
$I->click('Add User', '#content');
$I->submitForm('.form', [
'name' => 'ZZZ Test Administrator',
'email' => 'test@azuracast.com',
'auth_password' => 'CorrectBatteryStapleHorse',
]);
$I->seeCurrentUrlEquals('/admin/users');
$I->see('test@azuracast.com');
// Delete the secondary user
$I->click(\Codeception\Util\Locator::lastElement('.btn-danger'));
$I->seeCurrentUrlEquals('/admin/users');
$I->dontSee('test@azuracast.com');
*/
}
/**
* @before setupComplete
* @before login
*/
public function manageStations(FunctionalTester $I): void
{
$I->wantTo('Manage stations.');
$I->amOnPage('/admin/stations');
$I->seeResponseCodeIs(200);
/*
* TODO: Acceptance Testing with Vue Rendering
$I->see('Functional Test Radio');
$I->click('Edit');
$I->submitForm('.form', [
'name' => 'Modification Test Radio',
]);
$I->seeCurrentUrlEquals('/admin/stations');
$I->see('Modification Test Radio');
*/
}
/**
* @before setupComplete
* @before login
*/
public function manageSettings(FunctionalTester $I): void
{
$I->wantTo('Manage settings.');
$I->amOnPage('/admin/settings');
$I->seeResponseCodeIs(200);
$I->seeInTitle('System Settings');
}
}