From d7fec7169fd954ce2cefd9609bd254173ea61ea3 Mon Sep 17 00:00:00 2001 From: Vladimir Tomashevskiy Date: Sun, 8 Feb 2026 14:03:26 +0000 Subject: [PATCH] Tasks Module Stage 1: RBAC + Validation + Events Fix - Add canCreate/canEdit/canDelete checks in TasksController - Add input validation in store() and update() methods - Fix events naming: task.* (singular) instead of tasks.* - Add CSRF validation and parameter checks for API endpoints --- TASKS_MODULE_ROADMAP.md | 112 ++++++++++++++++++ .../Tasks/Controllers/TasksController.php | 96 +++++++++++++-- app/Modules/Tasks/Services/TaskService.php | 12 +- 3 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 TASKS_MODULE_ROADMAP.md diff --git a/TASKS_MODULE_ROADMAP.md b/TASKS_MODULE_ROADMAP.md new file mode 100644 index 0000000..03d3c7a --- /dev/null +++ b/TASKS_MODULE_ROADMAP.md @@ -0,0 +1,112 @@ +# План доработки модуля Tasks + +> На основе ТЗ п.3.6 + +--- + +## Этап 1: RBAC + валидация (Готово ✅) +- [x] `canCreate()` в create(), store() +- [x] `canEdit()` в edit(), update(), moveColumn(), complete(), reopen() +- [x] `canDelete()` в destroy() +- [x] Валидация в store(), update() +- [x] Исправлен нейминг событий: `task.*` (singular) + +--- + +## Этап 2: Подзадачи (Subtasks) + +### 2.1 Миграция +```php +task_subtasks (id, task_id, title, is_completed, order_index, created_at) +``` + +### 2.2 Модель + API +- TaskSubtaskModel +- addSubtask(), toggleSubtask(), deleteSubtask() + +### 2.3 View +- Отображение подзадач в task/show.twig +- Чекбоксы для toggle + +--- + +## Этап 3: Чек-листы + +### 3.1 Структура +``` +task_checklists (id, task_id, title) +task_checklist_items (id, checklist_id, text, is_completed, order_index) +``` + +--- + +## Этап 4: Вложения + +### 4.1 Миграция +```php +task_attachments (id, task_id, file_name, file_path, file_size, uploaded_by, created_at) +``` + +### 4.2 API +- uploadAttachment(), deleteAttachment() + +--- + +## Этап 5: Комментарии + @mentions + +### 5.1 Миграция +```php +task_comments (id, task_id, user_id, content, mentioned_users(JSON), created_at) +``` + +### 5.2 Обработка @mentions +- Парсинг `@user_id` из текста +- Создание уведомлений для упомянутых + +--- + +## Этап 6: Зависимости задач + +### 6.1 Миграция +```php +task_dependencies (id, blocking_task_id, blocked_task_id, type) +``` + +### 6.2 Логика +- Проверка при completeTask(): если есть незавершённые blocking tasks — ошибка + +--- + +## Этап 7: Интеграция CRM → Tasks + +### 7.1 Events +```php +Events::on('deal.created') → создать задачу "Первичный контакт" +Events::on('deal.stage_changed') → при этапе "КП" создать задачу +Events::on('deal.won') → создать задачу "Благодарность клиенту" +``` + +--- + +## Этап 8: Интеграция Booking → Tasks + +```php +Events::on('booking.created') → создать задачу "Подготовка к встрече" +``` + +--- + +## Приоритеты + +| Этап | Задача | Оценка | +|------|--------|--------| +| 1 | RBAC + валидация | 4ч ✅ | +| 2 | Подзадачи | 8ч | +| 3 | Чек-листы | 6ч | +| 4 | Вложения | 8ч | +| 5 | Комментарии + @mentions | 12ч | +| 6 | Зависимости | 6ч | +| 7 | CRM → Tasks | 8ч | +| 8 | Booking → Tasks | 4ч | + +**Всего:** ~56 часов diff --git a/app/Modules/Tasks/Controllers/TasksController.php b/app/Modules/Tasks/Controllers/TasksController.php index c05601b..ba7a80c 100644 --- a/app/Modules/Tasks/Controllers/TasksController.php +++ b/app/Modules/Tasks/Controllers/TasksController.php @@ -216,6 +216,10 @@ class TasksController extends BaseController */ public function create() { + if (!$this->access->canCreate('tasks')) { + return $this->forbiddenResponse('Нет прав для создания задач'); + } + $organizationId = $this->requireActiveOrg(); $boardId = (int) ($this->request->getGet('board') ?? 0); @@ -263,15 +267,33 @@ class TasksController extends BaseController */ public function store() { + if (!$this->access->canCreate('tasks')) { + return $this->forbiddenResponse('Нет прав для создания задач'); + } + $organizationId = $this->requireActiveOrg(); $userId = $this->getCurrentUserId(); + // Валидация входных данных + $validation = \Config\Services::validation(); + $validation->setRules([ + 'title' => 'required|min_length[3]|max_length[255]', + 'board_id' => 'required|integer', + 'column_id' => 'required|integer', + 'priority' => 'in_list[low,medium,high,urgent]', + 'due_date' => 'permit_empty|valid_date[Y-m-d]', + ]); + + if (!$validation->withRequest($this->request)->run()) { + return redirect()->back()->withErrors($validation->getErrors())->withInput(); + } + $data = [ 'organization_id' => $organizationId, - 'board_id' => $this->request->getPost('board_id'), - 'column_id' => $this->request->getPost('column_id'), - 'title' => $this->request->getPost('title'), - 'description' => $this->request->getPost('description'), + 'board_id' => (int) $this->request->getPost('board_id'), + 'column_id' => (int) $this->request->getPost('column_id'), + 'title' => trim($this->request->getPost('title')), + 'description' => trim($this->request->getPost('description')) ?: null, 'priority' => $this->request->getPost('priority') ?? 'medium', 'due_date' => $this->request->getPost('due_date') ?: null, ]; @@ -314,6 +336,10 @@ class TasksController extends BaseController */ public function edit(int $id) { + if (!$this->access->canEdit('tasks')) { + return $this->forbiddenResponse('Нет прав для редактирования задач'); + } + $organizationId = $this->requireActiveOrg(); $task = $this->taskService->getTask($id, $organizationId); @@ -353,14 +379,32 @@ class TasksController extends BaseController */ public function update(int $id) { + if (!$this->access->canEdit('tasks')) { + return $this->forbiddenResponse('Нет прав для редактирования задач'); + } + $organizationId = $this->requireActiveOrg(); $userId = $this->getCurrentUserId(); + // Валидация входных данных + $validation = \Config\Services::validation(); + $validation->setRules([ + 'title' => 'required|min_length[3]|max_length[255]', + 'board_id' => 'required|integer', + 'column_id' => 'required|integer', + 'priority' => 'in_list[low,medium,high,urgent]', + 'due_date' => 'permit_empty|valid_date[Y-m-d]', + ]); + + if (!$validation->withRequest($this->request)->run()) { + return redirect()->back()->withErrors($validation->getErrors())->withInput(); + } + $data = [ - 'board_id' => $this->request->getPost('board_id'), - 'column_id' => $this->request->getPost('column_id'), - 'title' => $this->request->getPost('title'), - 'description' => $this->request->getPost('description'), + 'board_id' => (int) $this->request->getPost('board_id'), + 'column_id' => (int) $this->request->getPost('column_id'), + 'title' => trim($this->request->getPost('title')), + 'description' => trim($this->request->getPost('description')) ?: null, 'priority' => $this->request->getPost('priority') ?? 'medium', 'due_date' => $this->request->getPost('due_date') ?: null, ]; @@ -383,6 +427,10 @@ class TasksController extends BaseController */ public function destroy(int $id) { + if (!$this->access->canDelete('tasks')) { + return $this->forbiddenResponse('Нет прав для удаления задач'); + } + $organizationId = $this->requireActiveOrg(); $userId = $this->getCurrentUserId(); @@ -396,11 +444,25 @@ class TasksController extends BaseController */ public function moveColumn() { + if (!$this->access->canEdit('tasks')) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Нет прав для изменения задач' + ]); + } + $organizationId = $this->requireActiveOrg(); $userId = $this->getCurrentUserId(); - $taskId = $this->request->getPost('id'); - $newColumnId = $this->request->getPost('column_id'); + $taskId = (int) $this->request->getPost('id'); + $newColumnId = (int) $this->request->getPost('column_id'); + + if (!$taskId || !$newColumnId) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Некорректные параметры' + ]); + } $result = $this->taskService->changeColumn($taskId, $newColumnId, $userId); @@ -418,6 +480,13 @@ class TasksController extends BaseController */ public function complete(int $id) { + if (!$this->access->canEdit('tasks')) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Нет прав для изменения задач' + ]); + } + $userId = $this->getCurrentUserId(); $result = $this->taskService->completeTask($id, $userId); @@ -429,6 +498,13 @@ class TasksController extends BaseController */ public function reopen(int $id) { + if (!$this->access->canEdit('tasks')) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Нет прав для изменения задач' + ]); + } + $userId = $this->getCurrentUserId(); $result = $this->taskService->reopenTask($id, $userId); diff --git a/app/Modules/Tasks/Services/TaskService.php b/app/Modules/Tasks/Services/TaskService.php index 0005394..d9949bd 100644 --- a/app/Modules/Tasks/Services/TaskService.php +++ b/app/Modules/Tasks/Services/TaskService.php @@ -44,7 +44,7 @@ class TaskService } // Генерируем событие - Events::trigger('tasks.created', $taskId, $data, $userId); + Events::trigger('task.created', $taskId, $data, $userId); } return $taskId; @@ -63,7 +63,7 @@ class TaskService $result = $this->taskModel->update($taskId, $data); if ($result) { - Events::trigger('tasks.updated', $taskId, $data, $userId); + Events::trigger('task.updated', $taskId, $data, $userId); } return $result; @@ -96,7 +96,7 @@ class TaskService $result = $this->taskModel->update($taskId, $data); if ($result) { - Events::trigger('tasks.column_changed', $taskId, $oldColumnId, $newColumnId, $userId); + Events::trigger('task.column_changed', $taskId, $oldColumnId, $newColumnId, $userId); } return $result; @@ -117,7 +117,7 @@ class TaskService ]); if ($result) { - Events::trigger('tasks.completed', $taskId, $userId); + Events::trigger('task.completed', $taskId, $userId); } return $result; @@ -138,7 +138,7 @@ class TaskService ]); if ($result) { - Events::trigger('tasks.reopened', $taskId, $userId); + Events::trigger('task.reopened', $taskId, $userId); } return $result; @@ -157,7 +157,7 @@ class TaskService $result = $this->taskModel->delete($taskId); if ($result) { - Events::trigger('tasks.deleted', $taskId, $userId); + Events::trigger('task.deleted', $taskId, $userId); } return $result;