This commit is contained in:
2026-02-04 23:31:53 +01:00
commit cd59ccd99b
2401 changed files with 56808 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
<?php
/**
* @var array $values
* @var string|null $formId
* @var bool $detailsOpenAll
* @var bool $isReadOnly
*/
use MintyPHP\Session;
$values = $values ?? [];
$formId = $formId ?? 'role-form';
$detailsOpenAll = $detailsOpenAll ?? false;
$isReadOnly = $isReadOnly ?? false;
$detailsOpenAttr = $detailsOpenAll ? 'open' : '';
$readonlyAttr = $isReadOnly ? 'readonly' : '';
?>
<form id="<?php e($formId); ?>" method="post">
<details name="basic-data" open>
<summary>Stammdaten</summary>
<hr>
<label for="description">
<span><?php e(t('Description')); ?></span>
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
</details>
<hr>
<?php if (!empty($permissions)): ?>
<details name="permissions" <?php e($detailsOpenAttr); ?>>
<summary><?php e(t('Permissions')); ?></summary>
<hr>
<?php multiSelectForm('role-permissions', 'permission_ids', 'Assigned permissions', 'Select permissions', $permissions, $selectedPermissionIds ?? [], $isReadOnly, ['description', 'key']); ?>
</details>
<hr>
<?php endif; ?>
<?php Session::getCsrfInput(); ?>
</form>

View File

@@ -0,0 +1,61 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Repository\PermissionRepository;
use MintyPHP\Repository\RolePermissionRepository;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_CREATE);
$errors = [];
$form = [
'description' => '',
];
$permissions = PermissionRepository::list();
$selectedPermissionIds = [];
if (isset($_POST['description'])) {
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
$result = RoleService::createFromAdmin($_POST, $currentUserId);
$form = $result['form'] ?? $form;
$errors = $result['errors'] ?? [];
$selectedPermissionIds = $_POST['permission_ids'] ?? $selectedPermissionIds;
if (!is_array($selectedPermissionIds)) {
$selectedPermissionIds = [$selectedPermissionIds];
}
$selectedPermissionIds = array_values(array_unique(array_map('intval', $selectedPermissionIds)));
if ($result['ok'] ?? false) {
$permissionIds = $_POST['permission_ids'] ?? [];
if (!is_array($permissionIds)) {
$permissionIds = [$permissionIds];
}
$permissionIds = array_values(array_unique(array_map('intval', $permissionIds)));
$createdId = (int) ($result['id'] ?? 0);
if ($createdId > 0) {
RolePermissionRepository::replaceForRole($createdId, $permissionIds);
}
$action = (string) ($_POST['action'] ?? 'create');
if ($action === 'create_close') {
Flash::success('Role created', 'admin/roles', 'role_created');
Router::redirect('admin/roles');
} else {
$uuid = (string) ($result['uuid'] ?? '');
if ($uuid !== '') {
$target = "admin/roles/edit/{$uuid}";
Flash::success('Role created', $target, 'role_created');
Router::redirect($target);
} else {
Flash::success('Role created', 'admin/roles', 'role_created');
Router::redirect('admin/roles');
}
}
}
}
Buffer::set('title', t('Create role'));

View File

@@ -0,0 +1,44 @@
<?php
/**
* @var array<int, string> $errors
* @var array $form
*/
?>
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/roles"><?php e(t('Roles')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create role')); ?>
</div>
<div class="app-details-titlebar">
<h1>
<a href="admin/roles" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
<?php e(t('Create role')); ?>
</h1>
<div class="app-details-titlebar-actions">
<button type="submit" form="role-form" name="action" value="create" class="secondary outline">
<?php e(t('Create')); ?>
</button>
<button type="submit" form="role-form" name="action" value="create_close" class="primary">
<?php e(t('Create & close')); ?>
</button>
</div>
</div>
<?php if (!empty($errors)): ?>
<div class="notice" data-variant="error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php e($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php
$values = $form ?? [];
$detailsOpenAll = true;
require __DIR__ . '/_form.phtml';
?>
</section>
</div>

View File

@@ -0,0 +1,43 @@
<?php
use MintyPHP\Router;
use MintyPHP\Support\Guard;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_VIEW);
$limit = (int) ($_GET['limit'] ?? 10);
$offset = (int) ($_GET['offset'] ?? 0);
$search = trim((string) ($_GET['search'] ?? ''));
$order = (string) ($_GET['order'] ?? 'description');
$dir = (string) ($_GET['dir'] ?? 'asc');
$result = RoleService::listPaged([
'limit' => $limit,
'offset' => $offset,
'search' => $search,
'order' => $order,
'dir' => $dir,
]);
$defaultRoleId = SettingService::getDefaultRoleId();
$rows = [];
foreach ($result['rows'] as $row) {
$roleId = (int) ($row['id'] ?? 0);
$rows[] = [
'id' => $row['id'] ?? null,
'uuid' => $row['uuid'] ?? '',
'is_default' => $roleId > 0 && $defaultRoleId === $roleId,
'description' => $row['description'] ?? '',
'created' => dt($row['created'] ?? ''),
'modified' => dt($row['modified'] ?? ''),
];
}
Router::json([
'data' => $rows,
'total' => $result['total'] ?? 0,
]);

View File

@@ -0,0 +1,29 @@
<?php
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_DELETE);
$uuid = trim((string) ($id ?? ''));
if ($uuid === '') {
Router::redirect('admin/roles');
}
$result = RoleService::deleteByUuid($uuid);
if (!($result['ok'] ?? false)) {
$error = $result['error'] ?? 'unknown';
if ($error === 'admin_role_protected') {
Flash::error('The Admin role cannot be deleted', 'admin/roles', 'admin_role_protected');
} else {
Flash::error('Role not found', 'admin/roles', 'role_not_found');
}
Router::redirect('admin/roles');
}
Flash::success('Role deleted', 'admin/roles', 'role_deleted');
Router::redirect('admin/roles');

View File

@@ -0,0 +1,87 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Repository\PermissionRepository;
use MintyPHP\Repository\RolePermissionRepository;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\UserService;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_VIEW);
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
if ($currentUserId > 0) {
PermissionService::getUserPermissions($currentUserId);
}
$uuid = trim((string) ($id ?? ''));
$role = $uuid !== '' ? RoleService::findByUuid($uuid) : null;
if (!$role) {
Flash::error('Role not found', 'admin/roles', 'role_not_found');
Router::redirect('admin/roles');
}
$roleId = (int) ($role['id'] ?? 0);
$creatorId = (int) ($role['created_by'] ?? 0);
if ($creatorId > 0) {
$creator = UserService::findById($creatorId);
if ($creator) {
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
$role['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
$role['created_by_uuid'] = $creator['uuid'] ?? null;
}
}
$modifierId = (int) ($role['modified_by'] ?? 0);
if ($modifierId > 0) {
$modifier = UserService::findById($modifierId);
if ($modifier) {
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
$role['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
$role['modified_by_uuid'] = $modifier['uuid'] ?? null;
}
}
$errors = [];
$form = $role;
$permissions = PermissionRepository::list();
$selectedPermissionIds = RolePermissionRepository::listPermissionIdsByRoleId($roleId);
if (isset($_POST['description'])) {
if (!PermissionService::userHas($currentUserId, PermissionService::ROLES_UPDATE)) {
Flash::error('Permission denied', "admin/roles/edit/{$uuid}", 'permission_denied');
Router::redirect("admin/roles/edit/{$uuid}");
return;
}
$result = RoleService::updateFromAdmin($roleId, $_POST, $currentUserId);
$form = $result['form'] ?? $form;
$errors = $result['errors'] ?? [];
$selectedPermissionIds = $_POST['permission_ids'] ?? $selectedPermissionIds;
if (!is_array($selectedPermissionIds)) {
$selectedPermissionIds = [$selectedPermissionIds];
}
$selectedPermissionIds = array_values(array_unique(array_map('intval', $selectedPermissionIds)));
if ($result['ok'] ?? false) {
$permissionIds = $_POST['permission_ids'] ?? [];
if (!is_array($permissionIds)) {
$permissionIds = [$permissionIds];
}
$permissionIds = array_values(array_unique(array_map('intval', $permissionIds)));
RolePermissionRepository::replaceForRole($roleId, $permissionIds);
$action = (string) ($_POST['action'] ?? 'save');
if ($action === 'save_close') {
Flash::success('Role updated', 'admin/roles', 'role_updated');
Router::redirect('admin/roles');
} else {
Flash::success('Role updated', "admin/roles/edit/{$uuid}", 'role_updated');
Router::redirect("admin/roles/edit/{$uuid}");
}
}
}
Buffer::set('title', t('Edit role'));

View File

@@ -0,0 +1,167 @@
<?php
/**
* @var array<int, string> $errors
* @var array $form
* @var array $role
*/
use MintyPHP\Session;
$values = $form ?? $role ?? [];
$isReadOnly = !can('roles.update');
?>
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/roles"><?php e(t('Roles')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Edit role')); ?>
</div>
<div class="app-details-titlebar">
<h1>
<a href="admin/roles" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
<?php e(t('Edit role')); ?>
</h1>
<div class="app-details-titlebar-actions">
<?php if (can('roles.delete')): ?>
<details class="dropdown">
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
<ul dir="rtl">
<li>
<form method="post" action="admin/roles/delete/<?php e($values['uuid'] ?? ''); ?>"
onsubmit="return confirm('<?php e(t('Delete this role?')); ?>');">
<?php Session::getCsrfInput(); ?>
<button type="submit" class="transparent">
<?php e(t('Delete')); ?>
</button>
</form>
</li>
</ul>
</details>
<?php endif; ?>
<?php if (can('roles.update')): ?>
<button type="submit" form="role-form" name="action" value="save" class="secondary outline">
<?php e(t('Save')); ?>
</button>
<button type="submit" form="role-form" name="action" value="save_close" class="primary">
<?php e(t('Save & close')); ?>
</button>
<?php endif; ?>
</div>
</div>
<?php if (!empty($errors)): ?>
<div class="app-details-errors">
<div class="notice" data-variant="error">
<ul>
<?php foreach ($errors as $error): ?>
<li><?php e($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$detailsOpenAll = true;
$isReadOnly = $isReadOnly ?? false;
require __DIR__ . '/_form.phtml';
?>
</section>
<aside id="app-details-aside-section">
<div class="app-details-aside-section">
<hgroup>
<h2><?php e($values['description'] ?? ''); ?></h2>
<p><?php e(t('Role')); ?></p>
</hgroup>
<hr>
<div class="grid">
<div>
<small><?php e(t('ID')); ?></small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['id'] ?? ''); ?>">
<?php e($values['id'] ?? '-'); ?>
</span>
</p>
</div>
</div>
<?php
$createdByLabel = $values['created_by_label'] ?? '';
$createdById = $values['created_by'] ?? null;
$createdByUuid = $values['created_by_uuid'] ?? null;
$modifiedByLabel = $values['modified_by_label'] ?? '';
$modifiedById = $values['modified_by'] ?? null;
$modifiedByUuid = $values['modified_by_uuid'] ?? null;
?>
<div class="grid">
<div>
<small><?php e(t('Created')); ?></small>
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Created by')); ?>
</small>
<p>
<?php if ($createdByLabel !== ''): ?>
<?php if ($createdByUuid): ?>
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
<?php else: ?>
<?php e($createdByLabel); ?>
<?php endif; ?>
<?php elseif ($createdById): ?>
<?php e('#' . $createdById); ?>
<?php else: ?>
-
<?php endif; ?>
</p>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Modified')); ?></small>
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Modified by')); ?>
</small>
<p>
<?php if ($modifiedByLabel !== ''): ?>
<?php if ($modifiedByUuid): ?>
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
<?php else: ?>
<?php e($modifiedByLabel); ?>
<?php endif; ?>
<?php elseif ($modifiedById): ?>
<?php e('#' . $modifiedById); ?>
<?php else: ?>
-
<?php endif; ?>
</p>
</div>
</div>
<div class="grid">
<div>
<small>
<?php e(t('UUID')); ?>
</small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
<?php
$uuidValue = (string) ($values['uuid'] ?? '');
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
e($uuidDisplay);
?>
</span>
</p>
</div>
</div>
</div>
</aside>
</div>

View File

@@ -0,0 +1,23 @@
<?php
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Service\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_VIEW);
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
if ($currentUserId > 0) {
PermissionService::getUserPermissions($currentUserId);
}
Buffer::set('title', t('Roles'));
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$csrfKey = Session::$csrfSessionKey;
$csrfToken = $_SESSION[$csrfKey] ?? '';
Buffer::set(
'grid_csrf',
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);

View File

@@ -0,0 +1,159 @@
<?php
use MintyPHP\Router;
$canCreateRoles = can('roles.create');
$canDeleteRoles = can('roles.delete');
?>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Roles')); ?>
</div>
<div class="app-list-titlebar">
<h1><?php e(t('Roles')); ?></h1>
<div class="app-list-titlebar-actions">
<button
class="outline secondary"
type="button"
data-toolbar-toggle
data-toolbar-target="#roles-toolbar"
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
>
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
</button>
<?php if ($canCreateRoles): ?>
<a role="button" class="primary" href="admin/roles/create">
<i class="bi bi-plus"></i> <?php e(t('Create role')); ?>
</a>
<?php endif; ?>
</div>
</div>
<div class="app-list-toolbar" id="roles-toolbar">
<input type="search" id="role-search" placeholder="<?php e(t('Search...')); ?>">
</div>
<div class="app-list-table">
<div id="roles-grid"></div>
</div>
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="module">
import { initToolbarToggles } from "<?php e(asset('js/components/app-toggle-toolbar.js')); ?>";
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
import { buildUrl, postAction, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
initToolbarToggles();
const appBase = "<?php e(localeBase()); ?>";
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
const deleteConfirm = "<?php e(t('Delete this role?')); ?>";
const buildActionUrl = (action, id) => buildUrl(appBase, `admin/roles/${action}/${id}`);
const initRolesGrid = () => {
const formatBadge = (value) => badgeHtml(gridjs, value);
const gridConfig = createServerGrid({
gridjs: window.gridjs,
container: '#roles-grid',
dataUrl: 'admin/roles/data',
appBase,
columns: [
{
name: "<?php e(t('Description')); ?>",
sort: true,
formatter: (cell) => {
if (!cell || typeof cell !== 'object') {
return gridjs.html(String(cell ?? ''));
}
const label = cell.label ?? '';
return gridjs.html(`${label}`);
}
},
{
name: "<?php e(t('Created')); ?>",
sort: true,
formatter: (cell) => formatBadge(cell)
},
{
name: "<?php e(t('Modified')); ?>",
sort: true,
formatter: (cell) => formatBadge(cell)
},
{
name: "UUID",
hidden: true
},
{
name: "ID",
hidden: true
}
],
sortColumns: ['description', 'created', 'modified', null, null],
paginationLimit: 10,
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
mapData: (data) => data.data.map((row) => [
{
label: row.description,
is_default: row.is_default ? 1 : 0
},
row.created,
row.modified,
row.uuid,
row.id
]),
actions: {
enabled: true,
label: "<?php e(t('Actions')); ?>",
getRowId: (row) => row?.cells?.[3]?.data,
items: [
{
key: 'edit',
type: 'link',
label: "<i class=\"bi bi-pencil-fill\"></i>",
href: ({ id }) => buildActionUrl('edit', id)
},
<?php if ($canDeleteRoles): ?>
{
key: 'delete',
label: "<i class=\"bi bi-trash3-fill\"></i>",
className: 'danger outline',
confirm: deleteConfirm
}
<?php endif; ?>
],
onAction: async ({ action, id, grid }) => {
if (action !== 'delete') return;
const url = buildActionUrl('delete', id);
const response = await postAction(url, csrf);
if (response.ok) {
grid.forceRender();
} else {
window.location.href = new URL('admin/roles', appBase).toString();
}
}
},
search: {
input: '#role-search',
param: 'search',
debounce: 250
},
urlSync: true,
rowDataset: (row) => ({
uuid: row?.cells?.[3]?.data
}),
rowDblClick: {
getUrl: (rowData) => {
const uuid = rowData?.cells?.[3]?.data;
return uuid ? new URL(`admin/roles/edit/${uuid}`, appBase).toString() : '';
}
}
});
if (!gridConfig || !gridConfig.grid) {
console.warn('Roles grid init failed');
return null;
}
return gridConfig;
};
initRolesGrid();
</script>