refactor : simplify codebase and fix critical issues
Backend: - Add MCP Serializer to centralize entity-to-array conversion (~300 lines deduped) - Fix race condition in task/ticket number generation (SELECT FOR UPDATE + transaction) - Add unique constraint on task (project_id, number) with migration - Fix MIME type validation: use server-detected finfo instead of client-supplied type - Add allowlist of permitted MIME types for uploads - Fix TaskDocumentDownloadController: allow ROLE_CLIENT access, add priority:1 - Fix notification sent even when ticket status unchanged - Remove redundant exception constructors - Simplify services (BookStackApi double fetch, TokenEncryptor, GiteaApi) - Consolidate duplicate checks in processors Frontend: - Fix useApi isHandlingUnauthorized scope (module-level to prevent double 401 redirect) - Fix client-tickets toast key copy-paste bug - Merge duplicated tasks service methods (getByProject + getByProjectArchived) - Extract shared uploadWithRelation helper in task-documents service - Extract formatFileSize utility from duplicated component code - Extract status transition logic into useClientTicketHelpers composable - Remove dead code (unused router, handleLogout, empty script blocks) - Merge duplicate watchers and onMounted calls - Normalize arrow functions to function declarations per convention Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,7 @@ final class BookStackApiService
|
||||
* Search for pages and books within a specific shelf.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Fetch the shelf's book IDs
|
||||
* 1. Fetch the shelf data (book IDs + slugs)
|
||||
* 2. Run two search queries (one for pages, one for books)
|
||||
* 3. Filter results: pages must belong to a book on the shelf, books must be on the shelf
|
||||
*
|
||||
@@ -67,17 +67,27 @@ final class BookStackApiService
|
||||
*/
|
||||
public function searchInShelf(int $shelfId, string $query): array
|
||||
{
|
||||
$bookIds = $this->getShelfBookIds($shelfId);
|
||||
$shelfData = $this->request('GET', sprintf('/api/shelves/%d', $shelfId));
|
||||
$books = $shelfData['books'] ?? [];
|
||||
|
||||
if (empty($bookIds)) {
|
||||
if (empty($books)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$bookIds = array_map(static fn (array $book): int => $book['id'], $books);
|
||||
$bookSlugs = [];
|
||||
foreach ($books as $book) {
|
||||
$bookSlugs[$book['id']] = $book['slug'] ?? '';
|
||||
}
|
||||
|
||||
// Update cache for getShelfBookIds
|
||||
$this->shelfBookCache[$shelfId] = $bookIds;
|
||||
|
||||
$config = $this->getConfiguration();
|
||||
$baseUrl = rtrim($config->getUrl() ?? '', '/');
|
||||
$trimmed = trim($query);
|
||||
|
||||
// BookStack search API accepts {type:X} for one type at a time — run two queries
|
||||
// BookStack search API accepts {type:X} for one type at a time -- run two queries
|
||||
$pageResults = $this->request('GET', '/api/search', [
|
||||
'query' => ['query' => $trimmed.' {type:page}', 'count' => 50],
|
||||
]);
|
||||
@@ -87,13 +97,6 @@ final class BookStackApiService
|
||||
|
||||
$allResults = array_merge($pageResults['data'] ?? [], $bookResults['data'] ?? []);
|
||||
|
||||
// Build a map of bookId → bookSlug for URL construction
|
||||
$shelfData = $this->request('GET', sprintf('/api/shelves/%d', $shelfId));
|
||||
$bookSlugs = [];
|
||||
foreach ($shelfData['books'] ?? [] as $book) {
|
||||
$bookSlugs[$book['id']] = $book['slug'] ?? '';
|
||||
}
|
||||
|
||||
$filtered = [];
|
||||
foreach ($allResults as $item) {
|
||||
$type = $item['type'] ?? '';
|
||||
@@ -101,23 +104,20 @@ final class BookStackApiService
|
||||
if ('page' === $type) {
|
||||
$bookId = $item['book_id'] ?? 0;
|
||||
if (in_array($bookId, $bookIds, true)) {
|
||||
$bookSlug = $bookSlugs[$bookId] ?? '';
|
||||
$filtered[] = [
|
||||
'id' => $item['id'],
|
||||
'type' => 'page',
|
||||
'name' => $item['name'] ?? '',
|
||||
'url' => $baseUrl.'/books/'.$bookSlug.'/page/'.$item['slug'],
|
||||
];
|
||||
}
|
||||
} elseif ('book' === $type) {
|
||||
if (in_array($item['id'], $bookIds, true)) {
|
||||
$filtered[] = [
|
||||
'id' => $item['id'],
|
||||
'type' => 'book',
|
||||
'name' => $item['name'] ?? '',
|
||||
'url' => $baseUrl.'/books/'.$item['slug'],
|
||||
'url' => $baseUrl.'/books/'.($bookSlugs[$bookId] ?? '').'/page/'.$item['slug'],
|
||||
];
|
||||
}
|
||||
} elseif ('book' === $type && in_array($item['id'], $bookIds, true)) {
|
||||
$filtered[] = [
|
||||
'id' => $item['id'],
|
||||
'type' => 'book',
|
||||
'name' => $item['name'] ?? '',
|
||||
'url' => $baseUrl.'/books/'.$item['slug'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,9 +126,10 @@ final readonly class GiteaApiService
|
||||
|
||||
$regex = sprintf('#^[^/]+/%s($|-.+)#', preg_quote($taskCode, '#'));
|
||||
|
||||
return array_values(array_filter($allBranches, static function (array $branch) use ($regex): bool {
|
||||
return 1 === preg_match($regex, $branch['name']);
|
||||
}));
|
||||
return array_values(array_filter(
|
||||
$allBranches,
|
||||
static fn (array $branch): bool => 1 === preg_match($regex, $branch['name']),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,12 +52,12 @@ final readonly class NotificationService
|
||||
return;
|
||||
}
|
||||
|
||||
$number = sprintf('CT-%03d', $ticket->getNumber());
|
||||
$statusLabel = $ticket->getStatus();
|
||||
$message = 'Nouveau statut : '.$statusLabel;
|
||||
$number = sprintf('CT-%03d', $ticket->getNumber());
|
||||
$statusComment = $ticket->getStatusComment();
|
||||
$message = 'Nouveau statut : '.$ticket->getStatus();
|
||||
|
||||
if (null !== $ticket->getStatusComment() && '' !== $ticket->getStatusComment()) {
|
||||
$message .= ' — '.$ticket->getStatusComment();
|
||||
if (null !== $statusComment && '' !== $statusComment) {
|
||||
$message .= ' — '.$statusComment;
|
||||
}
|
||||
|
||||
$notification = new Notification();
|
||||
|
||||
@@ -17,31 +17,10 @@ final class TokenEncryptor
|
||||
#[Autowire('%env(ENCRYPTION_KEY)%')]
|
||||
string $encryptionKey,
|
||||
) {
|
||||
if ('' === $encryptionKey) {
|
||||
$this->key = '';
|
||||
$this->configured = false;
|
||||
$key = $this->tryDecodeKey($encryptionKey);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$key = sodium_hex2bin($encryptionKey);
|
||||
} catch (SodiumException) {
|
||||
$this->key = '';
|
||||
$this->configured = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (SODIUM_CRYPTO_SECRETBOX_KEYBYTES !== mb_strlen($key, '8bit')) {
|
||||
$this->key = '';
|
||||
$this->configured = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->key = $key;
|
||||
$this->configured = true;
|
||||
$this->key = $key ?? '';
|
||||
$this->configured = null !== $key;
|
||||
}
|
||||
|
||||
public function encrypt(string $plaintext): string
|
||||
@@ -71,6 +50,25 @@ final class TokenEncryptor
|
||||
return $plaintext;
|
||||
}
|
||||
|
||||
private function tryDecodeKey(string $encryptionKey): ?string
|
||||
{
|
||||
if ('' === $encryptionKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$key = sodium_hex2bin($encryptionKey);
|
||||
} catch (SodiumException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (SODIUM_CRYPTO_SECRETBOX_KEYBYTES !== mb_strlen($key, '8bit')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
private function assertConfigured(): void
|
||||
{
|
||||
if (!$this->configured) {
|
||||
|
||||
Reference in New Issue
Block a user