fix(helpdesk): repair security-level persistence, enrichment, and list UX

Saving security levels appeared broken in both the domain detail view and
the domains grid. Root causes:

- Repository never unwrapped MintyPHP DB rows (nested by table name), so
  reads always returned defaults even though writes succeeded.
- Batch queries bound tenant_id last while SQL expected it first, so
  list/detail enrichment for the grid matched nothing.
- Detail page AJAX submit looked up CSRF via a non-existent data attribute
  and dropped the note field entirely.
- Endpoint used a hand-rolled CSRF check instead of Session::checkCsrfToken().

Fixes:
- DomainSecurityLevelRepository: use RepositoryArrayHelper::unwrap(List),
  swap param order in listLevelsByDomainNos / listDetailsByDomainNos,
  centralize table name in a const.
- security-level-data endpoint: canonical Session::checkCsrfToken(),
  unified JSON/PRG error path with proper HTTP status codes and flash.
- Detail page: drop broken AJAX hijack, rely on native form POST + PRG
  (GR-CORE-012).
- Grid list: refetch via gridRef.grid.forceRender() after dialog save
  instead of DOM-only mutation, so Grid.js internal state stays in sync.
- Add dedicated Note column next to the badge with ellipsis + title
  tooltip.
- Replace console.* in the dialog with showAsyncFlash for user-visible
  errors; add i18n keys (de/en) for all error messages.
- Drop unused postAction import and TENANT_ID_OTHER test constant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 14:04:21 +02:00
parent 6ac685084d
commit fb86c02427
9 changed files with 151 additions and 161 deletions

View File

@@ -84,6 +84,8 @@ require templatePath('partials/app-list-filters.phtml');
'administration' => t('Administration'),
'contractType' => t('Contract type'),
'securityLevel' => t('Security level'),
'note' => t('Note'),
'saveError' => t('Save failed'),
],
]); ?></script>
<script type="module" src="<?php e(assetVersion('modules/helpdesk/js/pages/helpdesk-domains-index.js')); ?>"></script>

View File

@@ -15,85 +15,76 @@ $request = requestInput();
$session = app(SessionStoreInterface::class)->all();
$tenantId = (int) ($session['current_tenant']['id'] ?? 0);
$userId = (int) ($session['user']['id'] ?? 0);
$service = app(DomainSecurityLevelService::class);
$isPost = $request->isMethod('POST');
$wantsJson = $request->wantsJson();
$domainNo = trim((string) ($isPost
? $request->body('domain_no', '')
: $request->query('domain_no', '')));
$redirectUrl = $domainNo !== ''
? 'helpdesk/domain/' . rawurlencode($domainNo)
: 'helpdesk/domains';
$fail = static function (string $error, int $status = 400) use ($wantsJson, $redirectUrl): void {
if ($wantsJson) {
http_response_code($status);
Router::json(['ok' => false, 'error' => $error]);
return;
}
Flash::error(t($error), 'helpdesk-domains', 'security_level_error');
Router::redirect($redirectUrl);
};
if ($tenantId <= 0) {
Router::json(['ok' => false, 'error' => 'No tenant context']);
$fail('No tenant context', 400);
return;
}
$domainNo = trim((string) ($request->body('domain_no', '') ?: $request->query('domain_no', '')));
if ($domainNo === '') {
Router::json(['ok' => false, 'error' => 'Missing domain number']);
$fail('Missing domain number', 400);
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));
if (!$isPost) {
$details = $service->getDetails($tenantId, $domainNo);
Router::json([
'ok' => true,
'level' => $details['level'],
'badge_variant' => DomainSecurityLevelService::getBadgeVariant($details['level']),
'note' => $details['note'],
]);
return;
}
$details = $service->getDetails($tenantId, $domainNo);
if (!Session::checkCsrfToken()) {
$fail('Invalid CSRF token', 403);
return;
}
Router::json([
'ok' => true,
'level' => $details['level'],
'badge_variant' => DomainSecurityLevelService::getBadgeVariant($details['level']),
'note' => $details['note'],
]);
$level = trim((string) $request->body('security_level', ''));
if (!in_array($level, DomainSecurityLevelService::getAllowedLevels(), true)) {
$fail('Invalid security level', 422);
return;
}
$note = trim((string) $request->body('note', ''));
if (!$service->setLevel($tenantId, $domainNo, $level, $userId > 0 ? $userId : null, $note !== '' ? $note : null)) {
$fail('Failed to save security level', 500);
return;
}
if ($wantsJson) {
Router::json([
'ok' => true,
'level' => $level,
'badge_variant' => DomainSecurityLevelService::getBadgeVariant($level),
'note' => $note,
]);
return;
}
Flash::success(t('Security level updated'), 'helpdesk-domains', 'security_level_saved');
Router::redirect($redirectUrl);