Feature: Security Level mit Notiz für Domains
Implementiert ein Dialog-basiertes UI zum Bearbeiten des Security Levels mit optionaler Notiz-Funktion für Helpdesk-Domains. Backend: - DomainSecurityLevelRepository: CRUD für security_levels mit note-Spalte - DomainSecurityLevelService: Business-Logik mit Batch-Enrichment - security-level-data() Endpoint: AJAX + Full-Page POST mit CSRF - Migration 010: note TEXT-Spalte hinzugefügt - Bugfix: findByTenantAndDomainNo gibt null bei leerem Array zurück Frontend (Domain-Liste): - Badge in Grid klickbar → öffnet Modal-Dialog - Dialog mit Level-Select + Notiz-Textarea - AJAX-Save mit Live-Badge-Update ohne Reload - Event-Delegation für dynamische Grid-Inhalte Frontend (Domain-Detail): - Notiz-Feld im Security-Level-Formular - PRG-Pattern mit Flash-Messages Core: - app-http.js: DEFAULT_HEADERS auf XMLHttpRequest für CSRF-Kompatibilität i18n: - Keys: Priority, Note, Optional note, Security level updated Tests: - DomainSecurityLevelServiceTest: CRUD + Batch-Enrichment
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
@@ -47,6 +49,14 @@ $customerName = (string) ($domain['Customer_Name'] ?? '');
|
||||
$domainState = (string) ($domain['State'] ?? '');
|
||||
$domainAdministration = (string) ($domain['Administration'] ?? '');
|
||||
|
||||
$session = app(SessionStoreInterface::class)->all();
|
||||
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
||||
$securityLevelService = app(DomainSecurityLevelService::class);
|
||||
$securityDetails = $tenantId > 0 ? $securityLevelService->getDetails($tenantId, $domainNo) : ['level' => 'normal', 'note' => ''];
|
||||
$securityLevel = $securityDetails['level'];
|
||||
$securityLevelNote = $securityDetails['note'];
|
||||
$securityLevelVariant = DomainSecurityLevelService::getBadgeVariant($securityLevel);
|
||||
|
||||
$pageTitle = $domainUrl !== '' ? $domainUrl : $domainNo;
|
||||
|
||||
Buffer::set('title', $pageTitle . ' — ' . t('Helpdesk'));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Session;
|
||||
|
||||
/**
|
||||
* @var string $domainNo
|
||||
* @var array $domain
|
||||
@@ -11,6 +13,9 @@
|
||||
* @var string $domainState
|
||||
* @var string $domainAdministration
|
||||
* @var string $pageTitle
|
||||
* @var string $securityLevel
|
||||
* @var string $securityLevelVariant
|
||||
* @var string $securityLevelNote
|
||||
*/
|
||||
|
||||
$domainNo = $domainNo ?? '';
|
||||
@@ -23,6 +28,9 @@ $customerName = $customerName ?? '';
|
||||
$domainState = $domainState ?? '';
|
||||
$domainAdministration = $domainAdministration ?? '';
|
||||
$pageTitle = $pageTitle ?? $domainNo;
|
||||
$securityLevel = $securityLevel ?? 'normal';
|
||||
$securityLevelVariant = $securityLevelVariant ?? 'neutral';
|
||||
$securityLevelNote = $securityLevelNote ?? '';
|
||||
|
||||
$stateVariantMap = [
|
||||
'Aktiv' => 'success',
|
||||
@@ -101,17 +109,50 @@ $stateVariant = $stateVariantMap[$domainState] ?? 'neutral';
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Contract Info -->
|
||||
<details class="app-details-card" id="domain-contract-section" open hidden>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Contract information')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<div id="domain-contract-info">
|
||||
<p class="text-muted"><?php e(t('Loading...')); ?></p>
|
||||
</div>
|
||||
<!-- Contract Info -->
|
||||
<details class="app-details-card" id="domain-contract-section" open hidden>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Contract information')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<div id="domain-contract-info">
|
||||
<p class="text-muted"><?php e(t('Loading...')); ?></p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Security Level -->
|
||||
<details class="app-details-card" open>
|
||||
<summary>
|
||||
<span class="app-details-card-summary-title"><?php e(t('Security level')); ?></span>
|
||||
</summary>
|
||||
<div class="app-details-card-container">
|
||||
<div id="domain-security-level-form">
|
||||
<form method="POST" action="<?php e(lurl('helpdesk/domains/security-level-data')); ?>" data-security-level-form>
|
||||
<input type="hidden" name="domain_no" value="<?php e($domainNo); ?>">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<div class="app-form-grid">
|
||||
<div class="app-form-group">
|
||||
<label for="domain-security-level-select"><?php e(t('Security level')); ?></label>
|
||||
<select name="security_level" id="domain-security-level-select" data-security-level-select>
|
||||
<option value="niedrig" <?php e($securityLevel === 'niedrig' ? 'selected' : ''); ?>><?php e(t('Low')); ?></option>
|
||||
<option value="normal" <?php e($securityLevel === 'normal' ? 'selected' : ''); ?>><?php e(t('Normal')); ?></option>
|
||||
<option value="hoch" <?php e($securityLevel === 'hoch' ? 'selected' : ''); ?>><?php e(t('High')); ?></option>
|
||||
<option value="kritisch" <?php e($securityLevel === 'kritisch' ? 'selected' : ''); ?>><?php e(t('Critical')); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="app-form-group">
|
||||
<label for="domain-security-level-note"><?php e(t('Note')); ?></label>
|
||||
<textarea name="note" id="domain-security-level-note" rows="3" placeholder="<?php e(t('Optional note...')); ?>"><?php e($securityLevelNote); ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-form-actions">
|
||||
<button type="submit" class="secondary outline small"><?php e(t('Save')); ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -166,6 +207,7 @@ $stateVariant = $stateVariantMap[$domainState] ?? 'neutral';
|
||||
<?php if ($domainAdministration !== ''): ?>
|
||||
<span class="badge" data-variant="neutral"><?php e($domainAdministration); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="badge" data-variant="<?php e($securityLevelVariant); ?>" id="domain-security-badge"><?php e(t(ucfirst($securityLevel))); ?></span>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainDetailService;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
@@ -26,4 +27,10 @@ $tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
||||
$service = app(DomainDetailService::class);
|
||||
$result = $service->loadDomainDetails($domainNo, $customerNo, $tenantId);
|
||||
|
||||
$securityLevelService = app(DomainSecurityLevelService::class);
|
||||
$securityDetails = $tenantId > 0 ? $securityLevelService->getDetails($tenantId, $domainNo) : ['level' => 'normal', 'note' => ''];
|
||||
$result['security_level'] = $securityDetails['level'];
|
||||
$result['security_level_variant'] = DomainSecurityLevelService::getBadgeVariant($securityDetails['level']);
|
||||
$result['security_level_note'] = $securityDetails['note'];
|
||||
|
||||
Router::json($result);
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
gridRequireGetRequest();
|
||||
|
||||
$session = app(SessionStoreInterface::class)->all();
|
||||
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
||||
|
||||
$filters = gridParseFiltersFromSchemaFile(__DIR__ . '/domains/filter-schema.php');
|
||||
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
@@ -17,6 +22,8 @@ $state = trim((string) ($filters['state'] ?? ''));
|
||||
$state = $state === 'all' ? '' : $state;
|
||||
$administration = trim((string) ($filters['administration'] ?? ''));
|
||||
$administration = $administration === 'all' ? '' : $administration;
|
||||
$securityLevel = trim((string) ($filters['security_level'] ?? ''));
|
||||
$securityLevel = $securityLevel === 'all' ? '' : $securityLevel;
|
||||
$order = (string) ($filters['order'] ?? 'Customer_Name');
|
||||
$dir = (string) ($filters['dir'] ?? 'asc');
|
||||
$limit = (int) ($filters['limit'] ?? 10);
|
||||
@@ -113,6 +120,20 @@ if ($administration !== '') {
|
||||
}));
|
||||
}
|
||||
|
||||
// Security level filter
|
||||
if ($securityLevel !== '' && $tenantId > 0) {
|
||||
$securityLevelService = app(DomainSecurityLevelService::class);
|
||||
$allDomainNos = array_map(static fn (array $d): string => trim((string) ($d['No'] ?? '')), $filtered);
|
||||
$allDomainNos = array_values(array_filter($allDomainNos, static fn (string $n): bool => $n !== ''));
|
||||
$filterLookup = $allDomainNos !== [] ? $securityLevelService->getAllLevelsForDomains($tenantId, $allDomainNos) : [];
|
||||
$filtered = array_values(array_filter($filtered, static function (array $domain) use ($securityLevel, $filterLookup): bool {
|
||||
$domainNo = trim((string) ($domain['No'] ?? ''));
|
||||
$currentLevel = $filterLookup[$domainNo] ?? 'normal';
|
||||
|
||||
return $currentLevel === $securityLevel;
|
||||
}));
|
||||
}
|
||||
|
||||
// Sorting
|
||||
$total = count($filtered);
|
||||
|
||||
@@ -127,6 +148,17 @@ usort($filtered, static function (array $a, array $b) use ($order, $dir): int {
|
||||
// Pagination
|
||||
$rows = array_slice($filtered, $offset, $limit);
|
||||
|
||||
// Security level enrichment for displayed rows
|
||||
$securityLevelLookup = [];
|
||||
if ($tenantId > 0) {
|
||||
$securityLevelService = app(DomainSecurityLevelService::class);
|
||||
$pagedDomainNos = array_map(static fn (array $d): string => trim((string) ($d['No'] ?? '')), $rows);
|
||||
$pagedDomainNos = array_values(array_filter($pagedDomainNos, static fn (string $n): bool => $n !== ''));
|
||||
if ($pagedDomainNos !== []) {
|
||||
$securityLevelLookup = $securityLevelService->getAllDetailsForDomains($tenantId, $pagedDomainNos);
|
||||
}
|
||||
}
|
||||
|
||||
// Row preparation
|
||||
$stateVariantMap = [
|
||||
'Aktiv' => 'success',
|
||||
@@ -139,9 +171,13 @@ $preparedRows = [];
|
||||
foreach ($rows as $domain) {
|
||||
$customerNo = trim((string) ($domain['Customer_No'] ?? ''));
|
||||
$domainState = (string) ($domain['State'] ?? '');
|
||||
$domainNo = (string) ($domain['No'] ?? '');
|
||||
$details = $securityLevelLookup[$domainNo] ?? null;
|
||||
$level = $details['level'] ?? 'normal';
|
||||
$note = $details['note'] ?? '';
|
||||
|
||||
$preparedRows[] = [
|
||||
'No' => (string) ($domain['No'] ?? ''),
|
||||
'No' => $domainNo,
|
||||
'Customer_No' => $customerNo,
|
||||
'Customer_Name' => (string) ($domain['Customer_Name'] ?? ''),
|
||||
'URL' => (string) ($domain['URL'] ?? ''),
|
||||
@@ -151,6 +187,9 @@ foreach ($rows as $domain) {
|
||||
'contract_type' => (string) ($domain['contract_type'] ?? ''),
|
||||
'contract_no' => (string) ($domain['contract_no'] ?? ''),
|
||||
'debitor_url' => $customerNo !== '' ? $debitorBaseUrl . rawurlencode($customerNo) : '',
|
||||
'security_level' => $level,
|
||||
'security_level_variant' => DomainSecurityLevelService::getBadgeVariant($level),
|
||||
'security_level_note' => $note,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -72,5 +72,21 @@ return gridFilterSchema([
|
||||
],
|
||||
'label_attributes' => ['data-filter-optional' => true],
|
||||
],
|
||||
[
|
||||
'key' => 'security_level',
|
||||
'type' => 'select',
|
||||
'label' => 'Security level',
|
||||
'input_id' => 'helpdesk-domains-security-level-filter',
|
||||
'default' => 'all',
|
||||
'normalize' => 'all_to_empty',
|
||||
'allowed' => [
|
||||
['id' => 'all', 'description' => 'All'],
|
||||
['id' => 'niedrig', 'description' => 'Low'],
|
||||
['id' => 'normal', 'description' => 'Normal'],
|
||||
['id' => 'hoch', 'description' => 'High'],
|
||||
['id' => 'kritisch', 'description' => 'Critical'],
|
||||
],
|
||||
'label_attributes' => ['data-filter-optional' => true],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -48,6 +48,12 @@ $filterChipMeta = [
|
||||
'default' => (string) ($schemaByKey['administration']['default'] ?? 'all'),
|
||||
'options' => gridOptionMapFromAllowed((array) ($schemaByKey['administration'] ?? [])),
|
||||
],
|
||||
'security_level' => [
|
||||
'label' => t('Security level'),
|
||||
'type' => 'select',
|
||||
'default' => (string) ($schemaByKey['security_level']['default'] ?? 'all'),
|
||||
'options' => gridOptionMapFromAllowed((array) ($schemaByKey['security_level'] ?? [])),
|
||||
],
|
||||
];
|
||||
|
||||
$settingsGateway = app(HelpdeskSettingsGateway::class);
|
||||
|
||||
@@ -32,10 +32,46 @@ require templatePath('partials/app-list-filters.phtml');
|
||||
<div id="helpdesk-domains-grid"></div>
|
||||
</div>
|
||||
|
||||
<dialog
|
||||
data-app-security-dialog
|
||||
aria-labelledby="sec-dialog-title"
|
||||
aria-describedby="sec-dialog-domain"
|
||||
>
|
||||
<article class="app-confirm-dialog app-dialog-sm">
|
||||
<header>
|
||||
<h2 id="sec-dialog-title"><?php e(t('Security level')); ?></h2>
|
||||
<button type="button" class="close" data-sec-dialog-close aria-label="<?php e(t('Close')); ?>" title="<?php e(t('Close')); ?>"></button>
|
||||
</header>
|
||||
<p id="sec-dialog-domain" class="muted"></p>
|
||||
<div class="app-form-grid">
|
||||
<?php $csrfKey = \MintyPHP\Session::$csrfSessionKey; $csrfToken = (string) ($_SESSION[$csrfKey] ?? ''); ?>
|
||||
<input type="hidden" name="<?php e($csrfKey); ?>" id="sec-dialog-csrf" value="<?php e($csrfToken); ?>">
|
||||
<div class="app-form-group">
|
||||
<label for="sec-dialog-level"><?php e(t('Security level')); ?></label>
|
||||
<select id="sec-dialog-level" name="security_level">
|
||||
<option value="niedrig"><?php e(t('Low')); ?></option>
|
||||
<option value="normal"><?php e(t('Normal')); ?></option>
|
||||
<option value="hoch"><?php e(t('High')); ?></option>
|
||||
<option value="kritisch"><?php e(t('Critical')); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="app-form-group">
|
||||
<label for="sec-dialog-note"><?php e(t('Note')); ?></label>
|
||||
<textarea id="sec-dialog-note" name="note" rows="3" placeholder="<?php e(t('Optional note...')); ?>"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<button type="button" class="secondary outline" data-sec-dialog-cancel><?php e(t('Cancel')); ?></button>
|
||||
<button type="button" class="primary" data-sec-dialog-save><?php e(t('Save')); ?></button>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
|
||||
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
||||
<script type="application/json" id="page-config-helpdesk-domains"><?php gridJsonForJs([
|
||||
'dataUrl' => lurl('helpdesk/domains-data'),
|
||||
'domainBaseUrl' => lurl('helpdesk/domain/'),
|
||||
'securityLevelDataUrl' => lurl('helpdesk/domains/security-level-data'),
|
||||
'gridLang' => gridLang(),
|
||||
'gridSearch' => $searchConfig,
|
||||
'filterSchema' => $clientFilterSchema,
|
||||
@@ -47,6 +83,7 @@ require templatePath('partials/app-list-filters.phtml');
|
||||
'state' => t('State'),
|
||||
'administration' => t('Administration'),
|
||||
'contractType' => t('Contract type'),
|
||||
'securityLevel' => t('Security level'),
|
||||
],
|
||||
]); ?></script>
|
||||
<script type="module" src="<?php e(assetVersion('modules/helpdesk/js/pages/helpdesk-domains-index.js')); ?>"></script>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Module\Helpdesk\HelpdeskAuthorizationPolicy;
|
||||
use MintyPHP\Module\Helpdesk\Service\DomainSecurityLevelService;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requireAbilityOrForbidden(HelpdeskAuthorizationPolicy::ABILITY_ACCESS);
|
||||
|
||||
$request = requestInput();
|
||||
$session = app(SessionStoreInterface::class)->all();
|
||||
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
|
||||
$userId = (int) ($session['user']['id'] ?? 0);
|
||||
|
||||
if ($tenantId <= 0) {
|
||||
Router::json(['ok' => false, 'error' => 'No tenant context']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$domainNo = trim((string) ($request->body('domain_no', '') ?: $request->query('domain_no', '')));
|
||||
|
||||
if ($domainNo === '') {
|
||||
Router::json(['ok' => false, 'error' => 'Missing domain number']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(DomainSecurityLevelService::class);
|
||||
|
||||
if ($request->method() === 'POST') {
|
||||
// CSRF check: Compare token from request body with session token
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfSession = (string) ($session[$csrfKey] ?? '');
|
||||
$csrfRequest = (string) $request->body($csrfKey, '');
|
||||
|
||||
if ($csrfRequest === '' || $csrfRequest !== $csrfSession) {
|
||||
// For AJAX requests, return JSON error
|
||||
if ($request->wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'Invalid CSRF token']);
|
||||
return;
|
||||
}
|
||||
// For full-page POST, redirect with flash error
|
||||
Flash::error(t('Invalid CSRF token'), 'helpdesk-domains', 'csrf');
|
||||
Router::redirect('helpdesk/domain/' . rawurlencode($domainNo));
|
||||
return;
|
||||
}
|
||||
|
||||
$level = trim((string) $request->body('security_level', ''));
|
||||
|
||||
if ($level === '') {
|
||||
Router::json(['ok' => false, 'error' => 'Missing security level']);
|
||||
return;
|
||||
}
|
||||
|
||||
$allowed = DomainSecurityLevelService::getAllowedLevels();
|
||||
if (!in_array($level, $allowed, true)) {
|
||||
Router::json(['ok' => false, 'error' => 'Invalid security level']);
|
||||
return;
|
||||
}
|
||||
|
||||
$note = trim((string) $request->body('note', ''));
|
||||
|
||||
$success = $service->setLevel($tenantId, $domainNo, $level, $userId, $note !== '' ? $note : null);
|
||||
|
||||
if (!$success) {
|
||||
Router::json(['ok' => false, 'error' => 'Failed to save security level']);
|
||||
return;
|
||||
}
|
||||
|
||||
// For AJAX requests, return JSON
|
||||
if ($request->wantsJson()) {
|
||||
Router::json([
|
||||
'ok' => true,
|
||||
'level' => $level,
|
||||
'badge_variant' => DomainSecurityLevelService::getBadgeVariant($level),
|
||||
'note' => $note,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// For full-page POST, redirect with flash success
|
||||
Flash::success(t('Security level updated'), 'helpdesk-domains', 'security_level_saved');
|
||||
Router::redirect('helpdesk/domain/' . rawurlencode($domainNo));
|
||||
return;
|
||||
}
|
||||
|
||||
$details = $service->getDetails($tenantId, $domainNo);
|
||||
|
||||
Router::json([
|
||||
'ok' => true,
|
||||
'level' => $details['level'],
|
||||
'badge_variant' => DomainSecurityLevelService::getBadgeVariant($details['level']),
|
||||
'note' => $details['note'],
|
||||
]);
|
||||
Reference in New Issue
Block a user