p96 follow up

This commit is contained in:
team 1
2026-05-11 15:39:00 +02:00
parent 3e678c659c
commit f098a1a244
25 changed files with 790 additions and 131 deletions

View File

@@ -73,6 +73,15 @@ security:
- { path: ^/admin/login$, roles: PUBLIC_ACCESS }
- { path: ^/admin/logout$, roles: PUBLIC_ACCESS }
- { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
- { path: ^/admin/system-logs, roles: ROLE_SUPER_ADMIN }
- { path: ^/admin/vector, roles: ROLE_SUPER_ADMIN }
- { path: '^/admin/system/(?:prompt|agent)', roles: ROLE_SUPER_ADMIN }
- { path: ^/admin/jobs/global-reindex, roles: ROLE_SUPER_ADMIN }
- { path: '^/admin/ingest-profiles/(?:create|activate|remove)', roles: ROLE_SUPER_ADMIN }
- { path: ^/admin/ingest-profiles, roles: ROLE_KNOWLEDGE_ADMIN }
- { path: ^/admin/model-config, roles: ROLE_KNOWLEDGE_ADMIN }
- { path: ^/admin/documents, roles: ROLE_EDITOR }
- { path: ^/admin/tags, roles: ROLE_EDITOR }
- { path: ^/admin, roles: ROLE_ADMIN_AREA }
- { path: ^/chat/login$, roles: PUBLIC_ACCESS }

View File

@@ -245,7 +245,7 @@ parameters:
hide_when_material_query_matches_current: true
shop_results:
- label: Preis anzeigen
prompt: Zeige mir die Preise zu {shop_price_query}.
prompt: 'Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: {shop_price_query}.'
action_type: price_details
hide_when_answer_matches_any:
- '/\b(?:preis(?:angabe|information|e)?|preise?)\b.{0,100}\b(?:nicht|kein(?:e|en)?|ohne)\b.{0,100}\b(?:angegeben|vorhanden|enthalten|ausgewiesen|gefunden|verfügbar|verfuegbar)\b/iu'

View File

@@ -1208,6 +1208,14 @@ parameters:
- kostet
- shopsuche
- shop-suche
- information
- informationen
- info
- infos
- details
- detail
- daten
- angaben
context_fallback_filter_terms:
- preis
- preise
@@ -1217,6 +1225,14 @@ parameters:
- grenzwert
- grenzwerte
- grenzwerten
- information
- informationen
- info
- infos
- details
- detail
- daten
- angaben
- welche
- gut
- geeignet
@@ -1286,6 +1302,7 @@ parameters:
- wuerde
- will
- brauche
- explizit
- benötigen
- benoetigen
- benötige

View File

@@ -0,0 +1,200 @@
# RetrieX Patch p95 - Admin Role Matrix Hardening
## Ziel
Dieser Patch gleicht die im Admin sichtbare Rollenlogik mit der serverseitigen Zugriffskontrolle ab. Rollen sollen nicht nur in der Userverwaltung auswählbar sein, sondern an den relevanten Routen, Controllern und UI-Aktionen konsistent wirksam werden.
## Ausgangslage
Nach p93/p94 waren Userverwaltung, Aktiv/Inaktiv-Login-Blocker und Access-Denied-Seiten funktionsfähig. Bei der Rollenprüfung fielen aber mehrere Inkonsistenzen auf:
- `ROLE_EDITOR` existierte in der Hierarchie, wurde aber kaum als fachliche Serverberechtigung genutzt.
- Einige Aktionen waren im UI nur für Super-Admins sichtbar, serverseitig aber nur durch den allgemeinen `/admin`-Zugriff geschützt.
- Indexierungsprofile konnten serverseitig nicht streng genug zwischen Ansicht und kritischen Aktionen unterscheiden.
- `admin_ingest_profile_remove` erlaubte noch `GET` als Methode.
- Sidebar und Dashboard zeigten Links bzw. Aktionen, die für die jeweilige Rolle später nur in 403 endeten.
## Umgesetzte Zielmatrix
| Bereich | Effektive Rolle |
| --- | --- |
| Chat, Ask/SSE, History, Frontend-Messages | `ROLE_CHAT_USER` |
| Admin-Dashboard, Guides, allgemeine Adminbasis, Ingest-Job-Ansicht | `ROLE_ADMIN_AREA` |
| Dokumente, Dokumentversionen, Tags, Tag-Zuweisungen, Dokument-Ingest | `ROLE_EDITOR` |
| Modell-/Retrieval-Konfiguration lesen/testen, Ingest-Profile ansehen | `ROLE_KNOWLEDGE_ADMIN` |
| Userverwaltung, Logs, System Prompt, System Agent, Reset/Delete, Global Reindex, Profil-/Modell-Umschaltungen | `ROLE_SUPER_ADMIN` |
Die bestehende Hierarchie bleibt erhalten:
```yaml
ROLE_SUPER_ADMIN: [ROLE_KNOWLEDGE_ADMIN, ROLE_EDITOR, ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER]
ROLE_KNOWLEDGE_ADMIN: [ROLE_EDITOR, ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER]
ROLE_EDITOR: [ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER]
ROLE_ADMIN_AREA: [ROLE_USER]
ROLE_CHAT_USER: [ROLE_USER]
```
## Geänderte Dateien
### Controller / Security
- `config/packages/security.yaml`
- ergänzt konkrete `access_control`-Regeln vor dem generischen `/admin`-Fallback
- schützt Dokument-/Tag-Bereiche mit `ROLE_EDITOR`
- schützt Modell-/Ingest-Konfigbereiche mit `ROLE_KNOWLEDGE_ADMIN`
- schützt kritische Systembereiche mit `ROLE_SUPER_ADMIN`
- `src/Security/ApplicationRoles.php`
- Dokumentation der zentralen Rollenquelle erweitert
- `src/Controller/Admin/DocumentController.php`
- Dokumentliste/-details und alle Dokumentpflegeaktionen nun `ROLE_EDITOR`
- Reset/Delete bleiben `ROLE_SUPER_ADMIN`
- Rollenstrings auf `ApplicationRoles`-Konstanten umgestellt
- `src/Controller/Admin/DocumentTagController.php`
- Tagbearbeitung auf Dokumentebene nun `ROLE_EDITOR`
- `src/Controller/Admin/TagController.php`
- Tagliste, Taganlage, Löschung und Zuweisung nun `ROLE_EDITOR`
- `src/Controller/Admin/IngestProfileController.php`
- Profilansicht nun `ROLE_KNOWLEDGE_ADMIN`
- Profilanlage, Aktivierung und Löschung nun `ROLE_SUPER_ADMIN`
- `remove` nur noch per `POST`
- Aktivieren/Löschen prüfen jetzt serverseitig CSRF-Token
- `src/Controller/Admin/ModelGenerationConfigController.php`
- `src/Controller/Admin/IngestJobController.php`
- `src/Controller/Admin/AdminVectorLogController.php`
- `src/Controller/Admin/AdminSystemLogController.php`
- `src/Controller/Admin/SystemAgentController.php`
- `src/Controller/Admin/SystemPromptController.php`
- `src/Controller/Admin/UserController.php`
- Rollenstrings auf zentrale `ApplicationRoles`-Konstanten umgestellt
### Templates
- `templates/admin/base.html.twig`
- Sidebar blendet Rollenbereiche passend aus:
- User: Super-Admin
- Dokumente/Tags: Editor
- System Prompt/Agent/Logs: Super-Admin
- KI-/LLM-Setup und Ingest-Profile: Knowledge-Admin
- `templates/admin/dashboard/index.html.twig`
- Global-Reindex-Button nur noch für Super-Admins sichtbar
- Nicht-Super-Admins sehen einen Hinweis statt eines Buttons, der in 403 läuft
- `templates/admin/document/index.html.twig`
- `templates/admin/document/show.html.twig`
- `templates/admin/document/new_version.html.twig`
- Dokumentpflege- und Versionsaktionen auf `ROLE_EDITOR` ausgerichtet
- Dokumentlöschung bleibt `ROLE_SUPER_ADMIN`
- `templates/admin/ingest_profile/list.html.twig`
- Profilanlage nur noch für Super-Admins sichtbar
- Aktivieren/Löschen bleiben Super-Admin-Aktionen
- `templates/admin/tag/index.html.twig`
- Taganlage, Zuweisung und Löschung nur für Editor sichtbar
- Nicht-Editor sehen keine mutierenden Aktionen
## Nicht geändert
- RAG-/Retrieval-Logik
- Scoring
- Shop-Matching
- PromptBuilder
- AgentRunner
- Chat-Antwortlogik
- User-CRUD-Fachlogik aus p93
- Error-Page-/Access-Denied-Grundlogik aus p94
## Lokale Checks
Ausgeführt im ZIP-Stand ohne `vendor/`:
```bash
find src/Controller/Admin src/Security src/Repository src/Service/Admin -type f -name '*.php' -print0 | xargs -0 -n1 php -l
python3 - <<'PY'
from pathlib import Path
import yaml
yaml.safe_load(Path('config/packages/security.yaml').read_text())
print('[OK] yaml parse')
PY
```
Zusätzlich wurde eine einfache Twig-Balance-Prüfung auf `{% if %}`, `{% for %}` und `{% block %}` für die geänderten Templates durchgeführt.
## Empfohlene Checks in der Zielumgebung
```bash
php bin/console cache:clear
php bin/console lint:yaml config/packages/security.yaml
php bin/console lint:twig templates/admin
php bin/console debug:router | grep admin_
php bin/console mto:agent:config:validate
php bin/console mto:agent:regression:test
php bin/console mto:agent:config:audit-source --details
```
## Manuelle Rollentests
### `ROLE_ADMIN_AREA`
Soll können:
- `/admin/dashboard`
- `/admin/guides`
- `/admin/jobs`
Soll verweigert bekommen:
- `/admin/documents`
- `/admin/tags`
- `/admin/model-config`
- `/admin/ingest-profiles`
- `/admin/users`
### `ROLE_EDITOR`
Soll können:
- Dokumente ansehen/anlegen
- Dokumentversionen hochladen
- Versionen aktivieren
- Ingest für Dokumentversionen starten
- Tags anlegen/löschen/zuweisen
Soll verweigert bekommen:
- Userverwaltung
- System Prompt
- System Agent
- Systemlogs / Vectorlog
- Global Reindex
- Dokumentlöschung / kompletter Reset
- Ingest-Profil aktivieren/löschen
### `ROLE_KNOWLEDGE_ADMIN`
Soll können:
- alles aus `ROLE_EDITOR`
- Modellkonfiguration ansehen/testen
- Ingest-Profile ansehen
Soll verweigert bekommen:
- Userverwaltung
- System Prompt/Agent
- Logs
- Profil-/Modell-Aktivierung und Löschung
- Global Reindex
- Reset/Delete
### `ROLE_SUPER_ADMIN`
Soll alles können.

View File

@@ -0,0 +1,197 @@
# RetrieX Patch 96 - Follow-up Product Identity & Weak History Guard
## Ziel
Dieser Patch haertet zwei beobachtete Folgefrage-Fehlerklassen, ohne Retrieval, Scoring, Shop-Ranking oder Shop-Matching fachlich umzubauen:
1. **Preis-Folgeaktionen nach mehreren sichtbaren Produkten** duerfen nicht mehr auf den ersten generischen Modellanker zurueckfallen.
- Fehlerfall: Antwort nennt `Testomat 2000 CLT` und `Testomat LAB CL`; der Button `Preis anzeigen` erzeugte nur `Testomat 2000`.
- Neu: Wenn mehrere in der Antwort sichtbare Produkte erkannt werden, wird die Preis-Folgeaktion aus den konkreten Produktidentitaeten mit Produktnummern gebaut.
2. **Referenzielle Shop-Nachfragen mit schwacher Query** wie `suche im Shop nach der Information` duerfen den Produktfokus aus der History nicht verlieren.
- Fehlerfall: Nach `Testomat 2000 THCL` wurde die Shopquery zu `information` statt zum Verlaufanker `testomat 2000 thcl`.
- Neu: Schwache referenzielle Shopqueries, die nur aus Meta-/Info-/Noise-Tokens bestehen, werden mit dem letzten Produktmodellanker aus der History ersetzt.
## Geaenderte Dateien
- `src/Agent/AgentRunner.php`
- `config/retriex/chat-messages.yaml`
- `config/retriex/genre.yaml`
## Technische Umsetzung
### 1. Preis-Folgeaktionen behalten mehrere konkrete Produktidentitaeten
`buildFollowUpActionPriceQuery()` faellt bei mehreren angezeigten Produkten nicht mehr auf `answer_anchor` zurueck. Stattdessen wird eine begrenzte Produktliste aus den sichtbaren Shopprodukten gebaut, inklusive Produktnummern.
Beispiel erwarteter Button-Prompt:
```text
Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: Testomat 2000® CLT 100137; Testomat® LAB CL 116106.
```
Der Prompt enthaelt bewusst `Produkt/Produkten` und `Preise`, damit die bestehende Product-List-Follow-up-Logik fuer mehrere Produkte greifen kann und einzelne Produktanker separat suchen kann.
### 2. Schwache referenzielle Shopqueries nutzen History-Modellanker
Neue Guard-Funktion:
```php
guardWeakReferentialShopQueryWithHistoryModelAnchor()
```
Sie greift nur, wenn:
- die aktuelle Frage referenziell genug ist, um History fuer Shopqueries zu nutzen,
- die Shopquery keinen eigenen Produktmodellanker enthaelt,
- der Prompt selbst keinen neuen Produktmodellanker enthaelt,
- es kein Product-List-Follow-up ist,
- es kein Indikator-/Zubehoer-/Reagenz-Follow-up ist,
- die Query nur aus schwachen Meta-/Info-/Noise-Tokens besteht,
- in der History ein Produktmodellanker gefunden wird.
Die schwachen Info-Woerter werden in `genre.yaml` gepflegt:
- `information`
- `informationen`
- `info`
- `infos`
- `details`
- `detail`
- `daten`
- `angaben`
## Bewusst nicht geaendert
- Keine neue fachliche Produktliste im PHP-Core.
- Keine neue Testomat-/THCL-/CLT-/LAB-CL-Sonderlogik im Core.
- Keine Aenderung an Retrieval, Scoring, Ranking, Shopware-Suche oder PromptBuilder-Faktenregeln.
- Bestehende Indikator-/Zubehoer-Follow-up-Guards bleiben priorisiert.
## Lokale Checks
Im Patch-Arbeitsverzeichnis ausgefuehrt:
```bash
php -l src/Agent/AgentRunner.php
python3 - <<'PY'
import yaml, pathlib
for path in pathlib.Path('config/retriex').glob('*.yaml'):
with path.open(encoding='utf-8') as f:
yaml.safe_load(f)
print('all retriex yaml OK')
PY
```
Ergebnis:
- `AgentRunner.php`: Syntax OK
- alle `config/retriex/*.yaml`: YAML OK
Nicht lokal ausfuehrbar, weil `vendor/` im ZIP nicht enthalten ist:
```bash
bin/console mto:agent:config:validate
bin/console mto:agent:regression:test
bin/console mto:agent:config:audit-source --details
bin/console mto:agent:config:audit-patterns --details
```
## Testprompts nach Einspielen
### Fehlerfall 1: Preis-Folgeaktion nach zwei Chlor-Geraeten
```text
mit welchem testomat kann ich freies chlor messen
```
Erwartung in der Antwort:
- Hauptprodukte bleiben fokussiert auf die passenden Chlor-Geraete, insbesondere:
- `Testomat 2000® CLT` / `100137`
- `Testomat® LAB CL` / `116106`
- Folgeaktion `Preis anzeigen` darf nicht mehr nur `Testomat 2000` materialisieren.
Danach den angezeigten Preis-Button klicken oder sinngleich testen:
```text
Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: Testomat 2000® CLT 100137; Testomat® LAB CL 116106.
```
Erwartung:
- Shopquery/Split-Lookups fokussieren die konkreten Produktidentitaeten.
- Es duerfen nicht breit alle `Testomat 2000` Varianten wie Fe, Antox, Br2, ClO2 oder Wartungskoffer als Hauptantwort erscheinen.
### Fehlerfall 2: schwache Info-Shopnachfrage verliert THCL-Anker
```text
welche grenzwerte kann der testomat 2000 thcl messen
```
Dann:
```text
suche im shop nach der information
```
Optional auch Tippfehler-Variante:
```text
such eim shop nach der information
```
Erwartung:
- Die finale Shopquery darf nicht `information` sein.
- Erwarteter Verlaufanker: `testomat 2000 thcl`.
- Shopantwort bleibt beim Geraet/Modellfokus und driftet nicht zu fremden Testomat-808-SiO2-Zubehoerteilen, Horiba-Testern oder allgemeinen Treffern ab.
### Gruener Gegenflow: Brauwasser bleibt stabil
```text
ich suche ein wasseranalyse messgerät für eine Brauerei, um das brauwasser messen zu können
```
Dann:
```text
begründe deine auswahl
```
Dann:
```text
was kostet das gerät denn
```
Erwartung:
- Auswahl bleibt auf `Testomat 2000® CAL` fokussiert, sofern die Begruendung CAL als Hauptempfehlung gesetzt hat.
- Preis-Follow-up bleibt `testomat 2000 cal gerät` bzw. entsprechend CAL-fokussiert.
- Keine Regression hin zu breiten `testomat 2000` Varianten.
### Bestehende Praezisionsregression: Indikator 300 bleibt stabil
```text
Was ist der niedrigste Grenzwert für die Wasserhärte, welcher mit einem Testomaten überwacht werden kann?
```
Dann:
```text
mit welchem indikator wird der wert gemessen
```
Dann:
```text
was kostet der indikator
```
Erwartung:
- `0,02 °dH` / `Testomat 808`
- `Indikatortyp 300`
- Shopquery weiterhin fokussiert, z. B. `testomat 808 300 indikator`
- Nur die zwei exakten Indikator-300-Produkte, keine `300 S`-Varianten und keine anderen Codes.

View File

@@ -336,6 +336,26 @@ final readonly class AgentRunner
$optimizedShopQuery = '';
}
$weakReferentialHistoryModelShopSearchQuery = $this->guardWeakReferentialShopQueryWithHistoryModelAnchor(
prompt: $originalPrompt,
shopSearchQuery: $shopSearchQuery,
commerceHistoryContext: $commerceHistoryContext
);
if ($weakReferentialHistoryModelShopSearchQuery !== $shopSearchQuery) {
$this->agentLogger->info('Replaced weak referential shop query with history model anchor', [
'userId' => $userId,
'prompt' => $prompt,
'routingPrompt' => $routingPrompt,
'optimizedShopQuery' => $optimizedShopQuery,
'shopSearchQuery' => $shopSearchQuery,
'weakReferentialHistoryModelShopSearchQuery' => $weakReferentialHistoryModelShopSearchQuery,
]);
$shopSearchQuery = $weakReferentialHistoryModelShopSearchQuery;
$optimizedShopQuery = '';
}
$productListAnchoredShopSearchQuery = $this->guardReferentialProductListShopQueryWithHistoryAnchors(
prompt: $originalPrompt,
shopSearchQuery: $shopSearchQuery,
@@ -4148,6 +4168,84 @@ final readonly class AgentRunner
: $modelAnchor;
}
private function guardWeakReferentialShopQueryWithHistoryModelAnchor(
string $prompt,
string $shopSearchQuery,
string $commerceHistoryContext
): string {
$shopSearchQuery = trim($shopSearchQuery);
if (
$shopSearchQuery === ''
|| trim($commerceHistoryContext) === ''
|| $this->referenceAnchorExtractor->extractFirstProductModelAnchor($prompt) !== ''
|| $this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== ''
|| $this->isReferentialProductListShopFollowUpPrompt($prompt)
) {
return $shopSearchQuery;
}
if (!$this->shouldUseCommerceHistoryForShopQuery($prompt)) {
return $shopSearchQuery;
}
// Accessory and consumable follow-ups have their own detail-anchor guard.
if ($this->containsConfiguredShopQueryAnchorTrigger(trim($prompt . ' ' . $shopSearchQuery))) {
return $shopSearchQuery;
}
if (!$this->isWeakReferentialHistoryModelShopQuery($shopSearchQuery)) {
return $shopSearchQuery;
}
$modelAnchor = $this->normalizeShopQueryAnchor(
$this->extractLatestHistoryProductModelAnchor($commerceHistoryContext)
);
if ($modelAnchor === '') {
return $shopSearchQuery;
}
return $this->queryAlreadyContainsAllAnchorTokens($shopSearchQuery, $modelAnchor)
? $shopSearchQuery
: $modelAnchor;
}
private function isWeakReferentialHistoryModelShopQuery(string $shopSearchQuery): bool
{
if ($this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== '') {
return false;
}
$tokens = $this->tokenizeShopQueryCandidate($shopSearchQuery);
if ($tokens === []) {
return true;
}
$weakTokens = $this->buildShopQueryTokenSet($this->mergeUniqueStrings(
$this->mergeUniqueStrings(
$this->getShopQueryMetaGuardTerms(),
$this->getShopQueryContextFallbackFilterTerms()
),
$this->mergeUniqueStrings(
$this->agentRunnerConfig->getShopQueryProductListFollowUpNoiseTerms(),
$this->agentRunnerConfig->getShopQueryStopwordCleanupTerms()
)
));
if ($weakTokens === []) {
return false;
}
foreach ($tokens as $token) {
if (!isset($weakTokens[$token])) {
return false;
}
}
return true;
}
private function isMainDeviceReferentialShopQueryPrompt(string $prompt): bool
{
$tokens = $this->tokenizeShopQueryCandidate($prompt);
@@ -6864,15 +6962,27 @@ final readonly class AgentRunner
return $this->buildFollowUpActionProductQuery($numberedProducts[0]);
}
if (count($numberedProducts) > 1) {
return $this->buildFollowUpActionProductListQuery($numberedProducts);
}
$focusedProducts = $this->filterFollowUpActionPrimaryDisplayedProducts($displayedProducts);
if (count($focusedProducts) === 1) {
return $this->buildFollowUpActionProductQuery($focusedProducts[0]);
}
if (count($focusedProducts) > 1) {
return $this->buildFollowUpActionProductListQuery($focusedProducts);
}
if (count($displayedProducts) === 1) {
return $this->buildFollowUpActionProductQuery($displayedProducts[0]);
}
if (count($displayedProducts) > 1) {
return $this->buildFollowUpActionProductListQuery($displayedProducts);
}
$answerAnchor = $this->buildFollowUpActionAnswerAnchor($answerText);
if ($answerAnchor !== '') {
return $answerAnchor;
@@ -6941,6 +7051,37 @@ final readonly class AgentRunner
return $this->normalizeOneLine(implode(' ', array_filter($parts, static fn(string $part): bool => trim($part) !== '')));
}
/**
* @param ShopProductResult[] $products
*/
private function buildFollowUpActionProductListQuery(array $products): string
{
$queries = [];
$seen = [];
$maxProducts = max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpMaxAnchors());
foreach ($products as $product) {
if (!$product instanceof ShopProductResult) {
continue;
}
$query = $this->buildFollowUpActionProductQuery($product);
$key = mb_strtolower($query, 'UTF-8');
if ($query === '' || isset($seen[$key])) {
continue;
}
$seen[$key] = true;
$queries[] = $query;
if (count($queries) >= $maxProducts) {
break;
}
}
return $this->normalizeOneLine(implode('; ', $queries));
}
private function buildFollowUpActionAnswerAnchor(string $answerText): string
{
$anchors = [];

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Security\ApplicationRoles;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -21,7 +22,7 @@ final class AdminSystemLogController extends AbstractController
#[Route('', name: 'admin_system_logs_index')]
public function index(): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
$files = [];
@@ -47,7 +48,7 @@ final class AdminSystemLogController extends AbstractController
)]
public function view(string $date): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if ($date === 'current') {
$safeFilename = 'system.log';

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Security\ApplicationRoles;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -14,7 +15,7 @@ final class AdminVectorLogController extends AbstractController
#[Route('/log', name: 'admin_vector_log')]
public function view(): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
// ⚠️ Pfad ggf. anpassen falls bei dir anders konfiguriert
$logFile = \dirname(__DIR__, 3) . '/var/log/vector_service.log';

View File

@@ -8,6 +8,7 @@ use App\Entity\Document;
use App\Entity\DocumentVersion;
use App\Entity\IngestJob;
use App\Entity\User;
use App\Security\ApplicationRoles;
use App\Service\DocumentService;
use App\Service\FormatText;
use App\Service\IngestJobService;
@@ -33,6 +34,8 @@ final class DocumentController extends AbstractController
#[Route('', name: 'admin_documents', methods: ['GET'])]
public function index(EntityManagerInterface $em): Response
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
$documents = $em->getRepository(Document::class)
->createQueryBuilder('d')
->leftJoin('d.versions', 'v')
@@ -56,6 +59,8 @@ final class DocumentController extends AbstractController
)]
public function show(string $id, EntityManagerInterface $em): Response
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
return $this->render('admin/document/show.html.twig', [
'document' => $this->findDocument($id, $em),
]);
@@ -70,6 +75,8 @@ final class DocumentController extends AbstractController
ParameterBagInterface $params,
EntityManagerInterface $em,
): Response {
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
if (!$request->isMethod('POST')) {
return $this->render('admin/document/new.html.twig');
}
@@ -150,6 +157,8 @@ final class DocumentController extends AbstractController
ParameterBagInterface $params,
FormatText $formatText,
): Response {
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
$document = $this->findDocument($id, $em);
if (!$request->isMethod('POST')) {
@@ -202,6 +211,8 @@ final class DocumentController extends AbstractController
DocumentService $documentService,
IngestJobService $jobService,
): RedirectResponse {
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
if (!$this->isCsrfTokenValid('activate_version_' . $versionId, (string) $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
@@ -260,6 +271,8 @@ final class DocumentController extends AbstractController
EntityManagerInterface $em,
IngestJobService $jobService,
): RedirectResponse {
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
if (!$this->isCsrfTokenValid('ingest_version_' . $versionId, (string) $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
@@ -329,7 +342,7 @@ final class DocumentController extends AbstractController
ParameterBagInterface $params,
Connection $connection,
): RedirectResponse {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if (!$this->isCsrfTokenValid('system_reset', (string) $request->request->get('_token'))) {
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
@@ -400,7 +413,7 @@ SQL;
IngestJobService $jobService,
LockService $lockService,
): RedirectResponse {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if (!$this->isCsrfTokenValid('delete_document_' . $id, (string) $request->request->get('_token'))) {
throw $this->createAccessDeniedException();

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\TagRebuildJob;
use App\Security\ApplicationRoles;
use App\Service\Admin\DocumentTagAdminService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -19,6 +20,8 @@ final class DocumentTagController extends AbstractController
#[Route('/{id}/tags', name: 'admin_document_tags_edit', methods: ['GET'])]
public function edit(string $id, DocumentTagAdminService $svc): Response
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
$id = trim($id);
try {
@@ -38,6 +41,8 @@ final class DocumentTagController extends AbstractController
#[Route('/{id}/tags/save', name: 'admin_document_tags_save', methods: ['POST'])]
public function save(string $id, Request $request, DocumentTagAdminService $svc): RedirectResponse
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
$id = trim($id);
if (!$this->isCsrfTokenValid('admin_document_tags_save_' . $id, (string) $request->request->get('_token'))) {

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Security\ApplicationRoles;
use App\Entity\IngestJob;
use App\Service\IngestJobService;
use Doctrine\ORM\EntityManagerInterface;
@@ -50,7 +51,7 @@ final class IngestJobController extends AbstractController
)]
public function status(string $id, EntityManagerInterface $em): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_ADMIN_AREA');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_ADMIN_AREA);
$job = $this->findJob($id, $em);
@@ -71,7 +72,7 @@ final class IngestJobController extends AbstractController
IngestJobService $jobService,
EntityManagerInterface $em,
): RedirectResponse {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if (!$this->isCsrfTokenValid('global_reindex', (string) $request->request->get('_token'))) {
$this->addFlash('danger', 'Ungültiges CSRF-Token.');

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\IngestProfile;
use App\Security\ApplicationRoles;
use App\Service\Admin\IngestProfileAdminService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -17,6 +18,8 @@ final class IngestProfileController extends AbstractController
#[Route('/', name: 'admin_ingest_profile_list')]
public function list(IngestProfileAdminService $svc): Response
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN);
$data = $svc->listData();
return $this->render('admin/ingest_profile/list.html.twig', $data);
@@ -25,6 +28,8 @@ final class IngestProfileController extends AbstractController
#[Route('/create', name: 'admin_ingest_profile_create', methods: ['GET', 'POST'])]
public function create(Request $request, IngestProfileAdminService $svc): Response
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if ($request->isMethod('POST')) {
try {
$svc->create([
@@ -49,8 +54,15 @@ final class IngestProfileController extends AbstractController
#[Route('/activate/{id}', name: 'admin_ingest_profile_activate', methods: ['POST'])]
public function activate(
IngestProfile $profile,
IngestProfileAdminService $svc
IngestProfileAdminService $svc,
Request $request
): Response {
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if (!$this->isCsrfTokenValid('activate_ingest_profile_' . $profile->getId(), (string) $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
try {
$svc->activate($profile);
$this->addFlash('success', 'Ingest-Profil wurde aktiviert.');
@@ -61,9 +73,15 @@ final class IngestProfileController extends AbstractController
return $this->redirectToRoute('admin_ingest_profile_list');
}
#[Route('/remove/{id}', name: 'admin_ingest_profile_remove', methods: ['POST', 'GET'])]
public function remove(string $id, IngestProfileAdminService $svc): Response
#[Route('/remove/{id}', name: 'admin_ingest_profile_remove', methods: ['POST'])]
public function remove(string $id, IngestProfileAdminService $svc, Request $request): Response
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if (!$this->isCsrfTokenValid('delete_ingest_profile_' . $id, (string) $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
try {
$svc->remove($id);
$this->addFlash('success', 'Ingest-Profil wurde entfernt.');

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Security\ApplicationRoles;
use App\Entity\ModelGenerationConfig;
use App\Service\Admin\ModelGenerationConfigAdminService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -17,7 +18,7 @@ final class ModelGenerationConfigController extends AbstractController
#[Route('/', name: 'admin_model_config_list')]
public function list(ModelGenerationConfigAdminService $svc): Response
{
$this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN);
return $this->render('admin/model_config/list.html.twig', [
'configs' => $svc->list(),
@@ -29,7 +30,7 @@ final class ModelGenerationConfigController extends AbstractController
Request $request,
ModelGenerationConfigAdminService $svc
): Response {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if ($request->isMethod('POST')) {
try {
@@ -50,7 +51,7 @@ final class ModelGenerationConfigController extends AbstractController
ModelGenerationConfig $config,
ModelGenerationConfigAdminService $svc
): Response {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
$svc->activate($config);
@@ -63,7 +64,7 @@ final class ModelGenerationConfigController extends AbstractController
Request $request,
ModelGenerationConfigAdminService $svc
): Response {
$this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN);
$prompt = '';
$results = [];
@@ -86,7 +87,7 @@ final class ModelGenerationConfigController extends AbstractController
Request $request,
ModelGenerationConfigAdminService $svc
): Response {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if (!$this->isCsrfTokenValid(
'delete_model_config_'.$config->getId(),

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Security\ApplicationRoles;
use App\Service\Admin\IndexNdjsonInspector;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_SUPER_ADMIN')]
#[IsGranted(ApplicationRoles::ROLE_SUPER_ADMIN)]
final class SystemAgentController extends AbstractController
{
#[Route('/admin/system/agent', name: 'admin_system_agent', methods: ['GET'])]

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Security\ApplicationRoles;
use App\Entity\SystemPrompt;
use App\Service\Admin\SystemPromptAdminService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_SUPER_ADMIN')]
#[IsGranted(ApplicationRoles::ROLE_SUPER_ADMIN)]
final class SystemPromptController extends AbstractController
{
#[Route('/admin/system/prompt', name: 'admin_system_prompt', methods: ['GET', 'POST'])]

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Entity\TagRebuildJob;
use App\Security\ApplicationRoles;
use App\Service\Admin\TagAdminService;
use App\Tag\TagTypes;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -19,6 +20,8 @@ final class TagController extends AbstractController
#[Route('', name: 'admin_tags_index', methods: ['GET'])]
public function index(TagAdminService $svc): Response
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
return $this->render('admin/tag/index.html.twig', [
...$svc->getIndexData(),
...$this->buildJobStatusViewData(),
@@ -28,6 +31,8 @@ final class TagController extends AbstractController
#[Route('/create', name: 'admin_tags_create', methods: ['POST'])]
public function create(Request $request, TagAdminService $svc): RedirectResponse
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
if (!$this->isCsrfTokenValid('admin_tag_create', (string) $request->request->get('_token'))) {
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
@@ -53,6 +58,8 @@ final class TagController extends AbstractController
#[Route('/{id}/delete', name: 'admin_tags_delete', methods: ['POST'])]
public function delete(string $id, Request $request, TagAdminService $svc): RedirectResponse
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
if (!$this->isCsrfTokenValid('admin_tag_delete_' . $id, (string) $request->request->get('_token'))) {
$this->addFlash('danger', 'Ungültiges CSRF-Token.');
@@ -72,6 +79,8 @@ final class TagController extends AbstractController
#[Route('/{id}/assign', name: 'admin_tags_assign', methods: ['GET', 'POST'])]
public function assign(string $id, Request $request, TagAdminService $svc): Response
{
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR);
$id = trim($id);
if ($request->isMethod('POST')) {

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Security\ApplicationRoles;
use App\Entity\User;
use App\Service\Admin\UserAdminService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -19,7 +20,7 @@ final class UserController extends AbstractController
#[Route('', name: 'admin_users_index', methods: ['GET'])]
public function index(Request $request, UserAdminService $users): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
$query = trim((string) $request->query->get('q', ''));
$status = (string) $request->query->get('status', 'all');
@@ -44,7 +45,7 @@ final class UserController extends AbstractController
#[Route('/create', name: 'admin_users_create', methods: ['GET', 'POST'])]
public function create(Request $request, UserAdminService $users): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if (!$request->isMethod('POST')) {
return $this->render('admin/user/create.html.twig', [
@@ -80,7 +81,7 @@ final class UserController extends AbstractController
#[Route('/{id}/edit', name: 'admin_users_edit', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['GET', 'POST'])]
public function edit(string $id, Request $request, UserAdminService $users): Response
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
try {
$user = $users->requireUser($id);
@@ -127,7 +128,7 @@ final class UserController extends AbstractController
#[Route('/{id}/toggle-active', name: 'admin_users_toggle_active', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['POST'])]
public function toggleActive(string $id, Request $request, UserAdminService $users): RedirectResponse
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN);
if (!$this->isCsrfTokenValid('admin_user_toggle_active_' . $id, (string) $request->request->get('_token'))) {
$this->addFlash('danger', 'Ungültiges CSRF-Token.');

View File

@@ -17,6 +17,11 @@ final class ApplicationRoles
public const ROLE_USER = 'ROLE_USER';
/**
* Roles that can be assigned from the Admin UI or console.
*
* Effective access is backed by config/packages/security.yaml and by
* controller-level guards for action-specific permissions.
*
* @return array<string, string>
*/
public static function assignableChoices(): array

View File

@@ -77,23 +77,27 @@
RAG Dokumente & Wissen
</div>
<a class="nav-link text-light {% if route starts with 'admin_document' %}active fw-bold{% endif %}"
href="{{ path('admin_documents') }}">
<i class="bi bi-card-list"></i> Dokumente
</a>
{% if is_granted('ROLE_EDITOR') %}
<a class="nav-link text-light {% if route starts with 'admin_document' %}active fw-bold{% endif %}"
href="{{ path('admin_documents') }}">
<i class="bi bi-card-list"></i> Dokumente
</a>
{# ------------------------- #}
{# Tags (Document Routing) #}
{# ------------------------- #}
<a class="nav-link text-light {% if route starts with 'admin_tags' %}active fw-bold{% endif %}"
href="{{ path('admin_tags_index') }}">
<i class="bi bi-tag-fill"></i> Tags
</a>
{# ------------------------- #}
{# Tags (Document Routing) #}
{# ------------------------- #}
<a class="nav-link text-light {% if route starts with 'admin_tags' %}active fw-bold{% endif %}"
href="{{ path('admin_tags_index') }}">
<i class="bi bi-tag-fill"></i> Tags
</a>
{% endif %}
<a class="nav-link text-light {% if route starts with 'admin_system_agent' %}active fw-bold{% endif %}"
href="{{ path('admin_system_agent') }}">
<i class="bi bi-robot"></i> Wissensbasis (Chunk-Index)
</a>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<a class="nav-link text-light {% if route starts with 'admin_system_agent' %}active fw-bold{% endif %}"
href="{{ path('admin_system_agent') }}">
<i class="bi bi-robot"></i> Wissensbasis (Chunk-Index)
</a>
{% endif %}
<hr class="border-secondary">
@@ -101,15 +105,19 @@
RAG System-Profile
</div>
<a class="nav-link text-light {% if route starts with 'admin_system_prompt' %}active fw-bold{% endif %}"
href="{{ path('admin_system_prompt') }}">
<i class="bi bi-chat-right-dots-fill"></i> System Prompt
</a>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<a class="nav-link text-light {% if route starts with 'admin_system_prompt' %}active fw-bold{% endif %}"
href="{{ path('admin_system_prompt') }}">
<i class="bi bi-chat-right-dots-fill"></i> System Prompt
</a>
{% endif %}
<a class="nav-link text-light {% if route starts with 'admin_ingest_profile' %}active fw-bold{% endif %}"
href="{{ path('admin_ingest_profile_list') }}">
<i class="bi bi-search"></i> Indexierungsprofile (Ingest)
</a>
{% if is_granted('ROLE_KNOWLEDGE_ADMIN') %}
<a class="nav-link text-light {% if route starts with 'admin_ingest_profile' %}active fw-bold{% endif %}"
href="{{ path('admin_ingest_profile_list') }}">
<i class="bi bi-search"></i> Indexierungsprofile (Ingest)
</a>
{% endif %}
<hr class="border-secondary">
@@ -117,14 +125,16 @@
KI-Endpunkte
</div>
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
href="{{ path('admin_model_config_list') }}">
<i class="bi bi-rocket-takeoff-fill"></i> KI-/LLM-Setup
</a>
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
href="{{ path('admin_model_config_list') }}#agentLiveTest">
<i class="bi bi-rocket-takeoff-fill"></i> KI-Agent Live-Test
</a>
{% if is_granted('ROLE_KNOWLEDGE_ADMIN') %}
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
href="{{ path('admin_model_config_list') }}">
<i class="bi bi-rocket-takeoff-fill"></i> KI-/LLM-Setup
</a>
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
href="{{ path('admin_model_config_list') }}#agentLiveTest">
<i class="bi bi-rocket-takeoff-fill"></i> KI-Agent Live-Test
</a>
{% endif %}
<hr class="border-secondary">
<div class="text-info text-uppercase small mb-2">
System-Guiide
@@ -142,14 +152,16 @@
href="{{ path('admin_jobs') }}">
<i class="bi bi-terminal"></i> Indexierungs-Log (Ingest Jobs)
</a>
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
href="{{ path('admin_vector_log') }}">
<i class="bi bi-terminal"></i> Vector-Log Python
</a>
<a class="nav-link text-light {% if route starts with 'admin_job' %}active fw-bold{% endif %}"
href="{{ path('admin_system_logs_index') }}">
<i class="bi bi-terminal"></i> System-Logs
</a>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<a class="nav-link text-light {% if route starts with 'admin_vector_log' %}active fw-bold{% endif %}"
href="{{ path('admin_vector_log') }}">
<i class="bi bi-terminal"></i> Vector-Log Python
</a>
<a class="nav-link text-light {% if route starts with 'admin_system_logs' %}active fw-bold{% endif %}"
href="{{ path('admin_system_logs_index') }}">
<i class="bi bi-terminal"></i> System-Logs
</a>
{% endif %}
</nav>
</aside>

View File

@@ -226,19 +226,25 @@
physischen Retrieval-Artefakte wieder gerade.
</div>
<form method="post"
action="{{ path('admin_global_reindex') }}"
onsubmit="return confirm('Global Reindex starten? Dies kann mehrere Minuten dauern.');">
{% if is_granted('ROLE_SUPER_ADMIN') %}
<form method="post"
action="{{ path('admin_global_reindex') }}"
onsubmit="return confirm('Global Reindex starten? Dies kann mehrere Minuten dauern.');">
<input type="hidden"
name="_token"
value="{{ csrf_token('global_reindex') }}">
<input type="hidden"
name="_token"
value="{{ csrf_token('global_reindex') }}">
<button type="submit"
class="btn btn-sm btn-outline-info">
Global Reindex starten
</button>
</form>
<button type="submit"
class="btn btn-sm btn-outline-info">
Global Reindex starten
</button>
</form>
{% else %}
<div class="alert alert-dark border border-secondary text-light small mb-0">
Global Reindex ist Super-Admins vorbehalten.
</div>
{% endif %}
{% if anyHealthIssue %}
<div class="alert alert-dark border border-warning text-light small mt-3 mb-0">

View File

@@ -14,10 +14,12 @@
</div>
</div>
<a href="{{ path('admin_document_new') }}"
class="btn btn-sm btn-outline-info">
Neues Dokument
</a>
{% if is_granted('ROLE_EDITOR') %}
<a href="{{ path('admin_document_new') }}"
class="btn btn-sm btn-outline-info">
Neues Dokument
</a>
{% endif %}
</div>
{% for message in app.flashes('success') %}
@@ -189,10 +191,12 @@
<td class="text-end">
<div class="d-flex justify-content-end flex-wrap gap-2">
<a class="btn btn-sm btn-outline-info"
href="{{ path('admin_document_tags_edit', {id: document.id}) }}">
Tags
</a>
{% if is_granted('ROLE_EDITOR') %}
<a class="btn btn-sm btn-outline-info"
href="{{ path('admin_document_tags_edit', {id: document.id}) }}">
Tags
</a>
{% endif %}
<a class="btn btn-sm btn-outline-light"
href="{{ path('admin_document_show', {id: document.id}) }}">

View File

@@ -138,7 +138,7 @@
</div>
</div>
{% if is_granted('ROLE_SUPER_ADMIN') %}
{% if is_granted('ROLE_EDITOR') %}
<div class="d-flex justify-content-end">
<button class="btn btn-outline-info">
Version hochladen

View File

@@ -13,10 +13,12 @@
</div>
<div class="d-flex flex-wrap gap-2">
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
class="btn btn-sm btn-outline-info">
Tags bearbeiten
</a>
{% if is_granted('ROLE_EDITOR') %}
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
class="btn btn-sm btn-outline-info">
Tags bearbeiten
</a>
{% endif %}
<a href="{{ path('admin_documents') }}"
class="btn btn-sm btn-outline-secondary">
@@ -96,26 +98,30 @@
</div>
</div>
{% if is_granted('ROLE_SUPER_ADMIN') %}
{% if is_granted('ROLE_EDITOR') or is_granted('ROLE_SUPER_ADMIN') %}
<hr class="border-secondary">
<div class="d-flex flex-wrap gap-2">
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
class="btn btn-sm btn-outline-info">
Neue Version
</a>
{% if is_granted('ROLE_EDITOR') %}
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
class="btn btn-sm btn-outline-info">
Neue Version
</a>
{% endif %}
<form method="post"
action="{{ path('admin_document_delete', {id: document.id}) }}"
class="d-inline"
onsubmit="return confirm('Dokument wirklich löschen? Der Inhalt wird per Delete-Job aus dem Index entfernt.');">
<input type="hidden"
name="_token"
value="{{ csrf_token('delete_document_' ~ document.id) }}">
<button class="btn btn-sm btn-outline-danger">
Dokument löschen
</button>
</form>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<form method="post"
action="{{ path('admin_document_delete', {id: document.id}) }}"
class="d-inline"
onsubmit="return confirm('Dokument wirklich löschen? Der Inhalt wird per Delete-Job aus dem Index entfernt.');">
<input type="hidden"
name="_token"
value="{{ csrf_token('delete_document_' ~ document.id) }}">
<button class="btn btn-sm btn-outline-danger">
Dokument löschen
</button>
</form>
{% endif %}
</div>
{% endif %}
</div>
@@ -128,10 +134,12 @@
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="text-info mb-0">Tags</h5>
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
class="btn btn-sm btn-outline-light">
Bearbeiten
</a>
{% if is_granted('ROLE_EDITOR') %}
<a href="{{ path('admin_document_tags_edit', {id: document.id}) }}"
class="btn btn-sm btn-outline-light">
Bearbeiten
</a>
{% endif %}
</div>
{% if document.tags is empty %}
@@ -172,7 +180,7 @@
</div>
</div>
{% if is_granted('ROLE_SUPER_ADMIN') %}
{% if is_granted('ROLE_EDITOR') %}
<a href="{{ path('admin_document_version_new', {id: document.id}) }}"
class="btn btn-sm btn-outline-info">
Neue Version
@@ -258,7 +266,7 @@
<td class="text-end">
<div class="d-flex justify-content-end flex-wrap gap-2">
{% if version.isActive %}
{% if version.ingestStatus in ['PENDING', 'FAILED'] and is_granted('ROLE_SUPER_ADMIN') %}
{% if version.ingestStatus in ['PENDING', 'FAILED'] and is_granted('ROLE_EDITOR') %}
<form method="post"
action="{{ path('admin_document_version_ingest', {versionId: version.id}) }}"
class="d-inline"
@@ -276,7 +284,7 @@
</span>
{% endif %}
{% else %}
{% if is_granted('ROLE_SUPER_ADMIN') %}
{% if is_granted('ROLE_EDITOR') %}
<form method="post"
action="{{ path('admin_document_version_activate', {versionId: version.id}) }}"
class="d-inline"

View File

@@ -28,10 +28,12 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3"><i class="bi bi-search"></i> Indexierungsprofile</h1>
<a class="btn btn-sm btn-outline-info"
href="{{ path('admin_ingest_profile_create') }}">
Neues Profil anlegen
</a>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<a class="btn btn-sm btn-outline-info"
href="{{ path('admin_ingest_profile_create') }}">
Neues Profil anlegen
</a>
{% endif %}
</div>
{# ========================================================= #}

View File

@@ -106,9 +106,10 @@
});
</script>
<div class="card bg-black border-secondary text-light mb-4 shadow-sm">
<div class="card-body">
<h5 class="text-info mb-3">Neuen Tag hinzufügen</h5>
{% if is_granted('ROLE_EDITOR') %}
<div class="card bg-black border-secondary text-light mb-4 shadow-sm">
<div class="card-body">
<h5 class="text-info mb-3">Neuen Tag hinzufügen</h5>
<form method="post" action="{{ path('admin_tags_create') }}" class="row g-3">
<input type="hidden" name="_token" value="{{ csrf_token('admin_tag_create') }}"/>
@@ -154,8 +155,9 @@
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<div class="card bg-black border-secondary text-light shadow-sm">
<div class="card-body">
@@ -207,23 +209,27 @@
</td>
<td>{{ tag.description ?: '-' }}</td>
<td class="text-end">
<a href="{{ path('admin_tags_assign', { id: tag.id }) }}"
class="btn btn-sm btn-outline-info me-2">
Zuweisen
</a>
{% if is_granted('ROLE_EDITOR') %}
<a href="{{ path('admin_tags_assign', { id: tag.id }) }}"
class="btn btn-sm btn-outline-info me-2">
Zuweisen
</a>
<form method="post"
action="{{ path('admin_tags_delete', {id: tag.id}) }}"
style="display:inline-block;">
<input type="hidden"
name="_token"
value="{{ csrf_token('admin_tag_delete_' ~ tag.id) }}"/>
<form method="post"
action="{{ path('admin_tags_delete', {id: tag.id}) }}"
style="display:inline-block;">
<input type="hidden"
name="_token"
value="{{ csrf_token('admin_tag_delete_' ~ tag.id) }}"/>
<button class="btn btn-sm btn-outline-danger"
onclick="return confirm('Tag wirklich löschen? Zuweisungen werden entfernt.')">
Löschen
</button>
</form>
<button class="btn btn-sm btn-outline-danger"
onclick="return confirm('Tag wirklich löschen? Zuweisungen werden entfernt.')">
Löschen
</button>
</form>
{% else %}
<span class="text-muted small">Nur Ansicht</span>
{% endif %}
</td>
</tr>
{% else %}