This commit is contained in:
F4ilji
2026-04-09 17:23:10 +05:00
parent 4bde0b10d0
commit 4aa4b9cadf
13 changed files with 437 additions and 125 deletions
+59 -1
View File
@@ -14,7 +14,24 @@ class Page extends Model
protected $guarded = false;
public function section() : BelongsTo
protected static function boot(): void
{
parent::boot();
// Auto-generate path when page is created
static::creating(function (Page $page) {
$page->generatePath();
});
// Only regenerate path if slug or parent section changed
static::updating(function (Page $page) {
if ($page->isDirty(['slug', 'sub_section_id'])) {
$page->generatePath();
}
});
}
public function section(): BelongsTo
{
return $this->belongsTo(SubSection::class, 'sub_section_id');
}
@@ -28,4 +45,45 @@ class Page extends Model
'content' => 'array',
'settings' => 'array',
];
/**
* Generate path for the page based on slug and parent section
*/
protected function generatePath(): void
{
// Don't regenerate path for registered pages (system routes)
if ($this->exists && $this->is_registered) {
return;
}
// Only generate path if slug is present
if (empty($this->slug)) {
return;
}
// Get sub_section_id from model attributes
$subSectionId = $this->sub_section_id;
if ($subSectionId === null) {
$this->path = $this->slug;
return;
}
// Fetch subSection with mainSection relationship
$subSection = SubSection::with('mainSection')->find($subSectionId);
if ($subSection === null) {
$this->path = $this->slug;
return;
}
$mainSection = $subSection->mainSection;
if ($mainSection === null) {
$this->path = $subSection->slug . '/' . $this->slug;
return;
}
$this->path = $mainSection->slug . '/' . $subSection->slug . '/' . $this->slug;
}
}
@@ -8,7 +8,6 @@ use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
class CreatePageAction
{
public function __construct(
private readonly GeneratePagePathAction $generatePagePathAction,
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
@@ -20,19 +19,11 @@ class CreatePageAction
*/
public function run(array $data): Page
{
$subSectionId = $data['sub_section_id'] ?? null;
// Генерируем path на основе subSection
$data['path'] = $this->generatePagePathAction->run($data['slug'], $subSectionId);
// Генерируем search_data из контента
if (!empty($data['content'])) {
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
}
// Удаляем sub_section_id — он не является полем модели Page
unset($data['sub_section_id']);
return Page::create($data);
}
}
@@ -1,27 +0,0 @@
<?php
namespace App\Containers\Dashboard\Actions\Pages;
use App\Containers\AppStructure\Models\SubSection;
class GeneratePagePathAction
{
public function run(string $slug, ?int $subSectionId = null): string
{
if ($subSectionId === null) {
return $slug;
}
$subSection = SubSection::with('mainSection')->find($subSectionId);
if ($subSection === null) {
return $slug;
}
if ($subSection->mainSection === null) {
return $subSection->slug . '/' . $slug;
}
return $subSection->mainSection->slug . '/' . $subSection->slug . '/' . $slug;
}
}
@@ -8,7 +8,6 @@ use App\Containers\Dashboard\Tasks\Content\GenerateSearchDataTask;
class UpdatePageAction
{
public function __construct(
private readonly GeneratePagePathAction $generatePagePathAction,
private readonly GenerateSearchDataTask $generateSearchDataTask,
) {}
@@ -26,17 +25,6 @@ class UpdatePageAction
$data['search_data'] = $this->generateSearchDataTask->run($data['content']);
}
// Если страница не зарегистрирована, перегенерируем path
if ($page->is_registered == false) {
$subSectionId = $data['sub_section_id'] ?? $page->sub_section_id;
$slug = $data['slug'] ?? $page->slug;
$data['path'] = $this->generatePagePathAction->run($slug, $subSectionId);
}
// Удаляем sub_section_id — он не является полем модели Page
unset($data['sub_section_id']);
$page->update($data);
return $page->fresh();
@@ -0,0 +1,98 @@
<?php
namespace App\Containers\Dashboard\Actions\Posts;
use Illuminate\Support\Facades\Log;
class DeploySiteAction
{
private string $lockFile = '/var/www/_deploy/deploy.lock';
private string $logFile = '/var/www/_deploy/deploy.log';
/**
* Создаёт файл-триггер для запуска деплоя
*
* @return array ['success' => bool, 'message' => string]
*/
public function run(): array
{
try {
// Проверяем, не запущен ли уже деплой
if (file_exists($this->lockFile)) {
$lockContent = file_get_contents($this->lockFile);
$lockData = json_decode($lockContent, true);
if ($lockData && isset($lockData['status']) && $lockData['status'] === 'running') {
return [
'success' => false,
'message' => 'Деплой уже запущен! Подождите завершения текущего процесса.',
];
}
}
// Создаём файл-триггер
$lockData = [
'status' => 'pending',
'started_at' => now()->toDateTimeString(),
'started_by' => auth()->id(),
];
file_put_contents($this->lockFile, json_encode($lockData, JSON_PRETTY_PRINT));
Log::info('Deploy triggered', ['user_id' => auth()->id()]);
return [
'success' => true,
'message' => 'Деплой запущен! Процесс обновления сайта начнётся в течение 1 минуты.',
];
} catch (\Exception $e) {
Log::error('Deploy trigger failed', ['exception' => $e->getMessage()]);
return [
'success' => false,
'message' => 'Ошибка при запуске деплоя: ' . $e->getMessage(),
];
}
}
/**
* Проверяет статус деплоя
*/
public function getStatus(): array
{
if (!file_exists($this->lockFile)) {
return ['status' => 'idle', 'message' => 'Деплой не запущен'];
}
$content = file_get_contents($this->lockFile);
$data = json_decode($content, true);
if (!$data) {
return ['status' => 'error', 'message' => 'Ошибка чтения статуса'];
}
// Читаем лог если есть
$log = '';
if (file_exists($this->logFile)) {
$log = file_get_contents($this->logFile);
}
return [
'status' => $data['status'] ?? 'unknown',
'started_at' => $data['started_at'] ?? null,
'completed_at' => $data['completed_at'] ?? null,
'message' => $data['message'] ?? '',
'log' => $log,
];
}
/**
* Очищает статус деплоя
*/
public function clearStatus(): void
{
if (file_exists($this->lockFile)) {
unlink($this->lockFile);
}
}
}
@@ -0,0 +1,77 @@
<?php
namespace App\Containers\Dashboard\UI\WEB\Controllers;
use App\Containers\Dashboard\Actions\Posts\DeploySiteAction;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class DeployController extends Controller
{
public function __construct(
private readonly DeploySiteAction $deploySiteAction,
) {}
/**
* Запускает деплой (создаёт файл-триггер)
*/
public function deploy(Request $request): JsonResponse
{
if (app()->environment() !== 'production') {
return response()->json([
'success' => false,
'message' => 'Деплой доступен только на production',
], 403);
}
if (!$request->user()->hasRole('admin')) {
return response()->json([
'success' => false,
'message' => 'Недостаточно прав для выполнения этой операции',
], 403);
}
$result = $this->deploySiteAction->run();
return response()->json($result);
}
/**
* Проверяет статус деплоя
*/
public function status(Request $request): JsonResponse
{
if (app()->environment() !== 'production') {
return response()->json(['status' => 'disabled'], 403);
}
if (!$request->user()->hasRole('admin')) {
return response()->json([
'success' => false,
'message' => 'Недостаточно прав',
], 403);
}
$status = $this->deploySiteAction->getStatus();
return response()->json($status);
}
/**
* Очищает статус деплоя
*/
public function clear(Request $request): JsonResponse
{
if (!$request->user()->hasRole('admin')) {
return response()->json([
'success' => false,
'message' => 'Недостаточно прав',
], 403);
}
$this->deploySiteAction->clearStatus();
return response()->json(['success' => true]);
}
}
@@ -10,6 +10,7 @@ use App\Containers\Dashboard\UI\WEB\Controllers\CategoryController as NewsCatego
use App\Containers\Dashboard\UI\WEB\Controllers\ContactWidgetController;
use App\Containers\Dashboard\UI\WEB\Controllers\CreateSliderController;
use App\Containers\Dashboard\UI\WEB\Controllers\CustomFormController;
use App\Containers\Dashboard\UI\WEB\Controllers\DeployController;
use App\Containers\Dashboard\UI\WEB\Controllers\DepartmentController;
use App\Containers\Dashboard\UI\WEB\Controllers\PageReferenceListController;
use App\Containers\Dashboard\UI\WEB\Controllers\DepartmentProgramController;
@@ -60,6 +61,9 @@ 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::post('/dashboard/deploy', [DeployController::class, 'deploy'])->name('dashboard.deploy');
Route::get('/dashboard/deploy/status', [DeployController::class, 'status'])->name('dashboard.deploy.status');
Route::post('/dashboard/deploy/clear', [DeployController::class, 'clear'])->name('dashboard.deploy.clear');
// CRUD постов
Route::prefix('/dashboard/posts')->name('dashboard.posts.')->group(function () {