diff --git a/app/Containers/Dashboard/Actions/Sveden/UpdateSvedenAction.php b/app/Containers/Dashboard/Actions/Sveden/UpdateSvedenAction.php new file mode 100644 index 0000000..9a068c6 --- /dev/null +++ b/app/Containers/Dashboard/Actions/Sveden/UpdateSvedenAction.php @@ -0,0 +1,42 @@ +ensureTempDirectory(); + + try { + $file->move(dirname($tempPath), basename($tempPath)); + + $result = $this->extractTask->run($tempPath); + + return $result; + } finally { + if (file_exists($tempPath)) { + unlink($tempPath); + } + } + } + + private function ensureTempDirectory(): void + { + $tempDir = storage_path('app/temp'); + + if (!is_dir($tempDir)) { + mkdir($tempDir, 0755, true); + } + } +} diff --git a/app/Containers/Dashboard/Tasks/Sveden/ExtractSvedenArchiveTask.php b/app/Containers/Dashboard/Tasks/Sveden/ExtractSvedenArchiveTask.php new file mode 100644 index 0000000..4aa7860 --- /dev/null +++ b/app/Containers/Dashboard/Tasks/Sveden/ExtractSvedenArchiveTask.php @@ -0,0 +1,153 @@ +open($zipPath); + + if ($openResult !== true) { + throw new \RuntimeException("Failed to open ZIP archive (code: {$openResult})"); + } + + $this->validateZipEntries($zip); + + $rootFolders = $this->getRootFolders($zip); + $allowedFolders = array_intersect($rootFolders, self::ALLOWED_FOLDERS); + + if (empty($allowedFolders)) { + $zip->close(); + throw new \RuntimeException( + 'Archive does not contain sveden or abitur folders. Found: ' . implode(', ', $rootFolders ?: ['none']) + ); + } + + $updated = []; + + foreach ($allowedFolders as $folder) { + $this->extractFolder($zip, $folder); + $updated[] = $folder; + } + + $zip->close(); + + return [ + 'success' => true, + 'updated' => $updated, + 'skipped' => array_diff($rootFolders, self::ALLOWED_FOLDERS), + ]; + } + + private function getRootFolders(ZipArchive $zip): array + { + $folders = []; + + for ($i = 0; $i < $zip->numFiles; $i++) { + $name = $zip->getNameIndex($i); + $parts = explode('/', $name); + + if (count($parts) > 1 && !empty($parts[0])) { + $folders[$parts[0]] = true; + } + } + + return array_keys($folders); + } + + private function extractFolder(ZipArchive $zip, string $folderName): void + { + $destination = public_path($folderName); + + if (!is_dir($destination)) { + mkdir($destination, 0755, true); + } + + for ($i = 0; $i < $zip->numFiles; $i++) { + $name = $zip->getNameIndex($i); + + if (strpos($name, $folderName . '/') !== 0) { + continue; + } + + $relativePath = substr($name, strlen($folderName . '/')); + + if (empty($relativePath)) { + continue; + } + + $targetPath = $destination . '/' . $relativePath; + + $this->validateFileExtension($relativePath); + + if (substr($name, -1) === '/') { + if (!is_dir($targetPath)) { + mkdir($targetPath, 0755, true); + } + } else { + $parentDir = dirname($targetPath); + if (!is_dir($parentDir)) { + mkdir($parentDir, 0755, true); + } + + $content = $zip->getFromIndex($i); + file_put_contents($targetPath, $content); + } + } + } + + private function validateFileExtension(string $filePath): void + { + $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + + if (in_array($extension, self::DANGEROUS_EXTENSIONS, true)) { + throw new \RuntimeException( + "Potentially dangerous file detected: {$filePath}" + ); + } + } + + private function validateZipEntries(ZipArchive $zip): void + { + for ($i = 0; $i < $zip->numFiles; $i++) { + $filename = $zip->getNameIndex($i); + + if (strpos($filename, '..') !== false) { + throw new \RuntimeException( + "Potentially malicious ZIP entry detected: {$filename}" + ); + } + + if (preg_match('/^[a-zA-Z]:/', $filename)) { + throw new \RuntimeException( + "Potentially malicious ZIP entry detected: {$filename}" + ); + } + + if (strpos($filename, '/') === 0) { + throw new \RuntimeException( + "Potentially malicious ZIP entry detected: {$filename}" + ); + } + } + } +} diff --git a/app/Containers/Dashboard/UI/WEB/Controllers/SvedenController.php b/app/Containers/Dashboard/UI/WEB/Controllers/SvedenController.php new file mode 100644 index 0000000..68af7f1 --- /dev/null +++ b/app/Containers/Dashboard/UI/WEB/Controllers/SvedenController.php @@ -0,0 +1,41 @@ +validate([ + 'archive' => ['required', 'file', 'mimes:zip', 'max:102400'], + ]); + + try { + $result = $this->updateAction->run($request->file('archive')); + + return response()->json($result); + } catch (\Throwable $e) { + report($e); + + return response()->json([ + 'success' => false, + 'message' => 'Ошибка при обновлении: ' . $e->getMessage(), + ], 500); + } + } +} diff --git a/app/Containers/Dashboard/UI/WEB/Routes/web.php b/app/Containers/Dashboard/UI/WEB/Routes/web.php index 36552f8..2e73994 100755 --- a/app/Containers/Dashboard/UI/WEB/Routes/web.php +++ b/app/Containers/Dashboard/UI/WEB/Routes/web.php @@ -27,6 +27,7 @@ use App\Containers\Dashboard\UI\WEB\Controllers\EditSliderController; use App\Containers\Dashboard\UI\WEB\Controllers\FacultyController; use App\Containers\Dashboard\UI\WEB\Controllers\FacultyWorkerController; use App\Containers\Dashboard\UI\WEB\Controllers\IndexDashboardController; +use App\Containers\Dashboard\UI\WEB\Controllers\IntegrationCredentialsController; use App\Containers\Dashboard\UI\WEB\Controllers\JournalIssueController; use App\Containers\Dashboard\UI\WEB\Controllers\MainSectionController; use App\Containers\Dashboard\UI\WEB\Controllers\PageController; @@ -36,6 +37,7 @@ use App\Containers\Dashboard\UI\WEB\Controllers\ProcessMixedFilesController; use App\Containers\Dashboard\UI\WEB\Controllers\PublishPostController; use App\Containers\Dashboard\UI\WEB\Controllers\QuickUploadController; use App\Containers\Dashboard\UI\WEB\Controllers\ScheduleController; +use App\Containers\Dashboard\UI\WEB\Controllers\SvedenController; use App\Containers\Dashboard\UI\WEB\Controllers\SliderController; use App\Containers\Dashboard\UI\WEB\Controllers\StoreFilesController; use App\Containers\Dashboard\UI\WEB\Controllers\StoreSlideController; @@ -61,10 +63,16 @@ Route::post('/dashboard/logout', [AuthenticatedSessionController::class, 'destro // Authenticated dashboard routes Route::middleware(['access-check', 'dashboard.auth'])->group(function () { Route::get('/dashboard', IndexDashboardController::class)->name('dashboard.index'); + Route::get('/dashboard/deploy', [DeployController::class, 'index'])->name('dashboard.deploy.index'); Route::post('/dashboard/deploy', [DeployController::class, 'deploy'])->name('dashboard.deploy'); Route::get('/dashboard/deploy/status', [DeployController::class, 'status'])->name('dashboard.deploy.status'); + Route::get('/dashboard/deploy/log', [DeployController::class, 'log'])->name('dashboard.deploy.log'); + Route::get('/dashboard/deploy/history', [DeployController::class, 'history'])->name('dashboard.deploy.history'); Route::post('/dashboard/deploy/clear', [DeployController::class, 'clear'])->name('dashboard.deploy.clear'); + Route::get('/dashboard/sveden', [SvedenController::class, 'index'])->name('dashboard.sveden'); + Route::post('/dashboard/sveden', [SvedenController::class, 'store'])->name('dashboard.sveden.store'); + // CRUD постов Route::prefix('/dashboard/posts')->name('dashboard.posts.')->group(function () { Route::get('/', [PostController::class, 'index'])->name('index'); @@ -406,6 +414,16 @@ Route::middleware(['access-check', 'dashboard.auth'])->group(function () { Route::put('/{pageReferenceList}', [PageReferenceListController::class, 'update'])->name('update'); Route::delete('/{pageReferenceList}', [PageReferenceListController::class, 'destroy'])->name('destroy'); }); + + // CRUD интеграционных ключей + Route::prefix('/dashboard/integration-credentials')->name('dashboard.integration-credentials.')->group(function () { + Route::get('/', [IntegrationCredentialsController::class, 'index'])->name('index'); + Route::get('/create', [IntegrationCredentialsController::class, 'create'])->name('create'); + Route::post('/', [IntegrationCredentialsController::class, 'store'])->name('store'); + Route::get('/{credential}/edit', [IntegrationCredentialsController::class, 'edit'])->name('edit'); + Route::put('/{credential}', [IntegrationCredentialsController::class, 'update'])->name('update'); + Route::delete('/{credential}', [IntegrationCredentialsController::class, 'destroy'])->name('destroy'); + }); }); diff --git a/resources/js/Pages/Dashboard/Components/quickActionsConfig.js b/resources/js/Pages/Dashboard/Components/quickActionsConfig.js index e78b839..0fff24a 100644 --- a/resources/js/Pages/Dashboard/Components/quickActionsConfig.js +++ b/resources/js/Pages/Dashboard/Components/quickActionsConfig.js @@ -25,4 +25,10 @@ export const quickActions = [ href: null, icon: 'upload', }, + { + label: 'Обновить Sveden', + route: 'dashboard.sveden', + href: null, + icon: 'cog', + }, ]; diff --git a/resources/js/Pages/Dashboard/Sveden.vue b/resources/js/Pages/Dashboard/Sveden.vue new file mode 100644 index 0000000..5b8a3ce --- /dev/null +++ b/resources/js/Pages/Dashboard/Sveden.vue @@ -0,0 +1,252 @@ + + +