CRUD зон/устройств/правил автоматизации + RBAC owner/viewer

Policies (ZonePolicy/DevicePolicy/AutomationRulePolicy): не multi-tenant
изоляция по user_id, а household-wide RBAC — все авторизованные видят одни
и те же данные, только owner может создавать/менять/удалять. user_id на
записи — это created_by, а не граница видимости.

Form Requests с authorize() через политики; DeviceRequest учитывает unique
external_id с ignore на update. AutomationRuleRequest: action_type остаётся
строкой (не enum), т.к. это открытый список из device_types.capabilities;
для действия set_level в форме есть отдельное поле "level" (Alpine.js
переключает его видимость), которое контроллер мапит в action_params —
без сырого JSON-редактора для MVP.

Контроллеры без show (index+create/edit достаточно), Blade-формы на базе
Breeze-компонентов. Навигация и дашборд (счётчики зон/устройств/активных
правил) обновлены. Добавлен DemoGrowboxSeeder (демо-зона с датчиком,
вентилятором и правилом) и viewer-пользователь в сидер.

Восстановлен трейт AuthorizesRequests в базовом Controller (Laravel 11+
убрал его по умолчанию) — нужен для $this->authorize().

Проверено: 33/33 теста (включая новые ZoneControllerTest,
AutomationRuleControllerTest — маппинг level→action_params, 403 для
viewer), полный CRUD-цикл вживую через браузер под owner (создание зоны,
правила с set_level, Alpine-переключение поля "Уровень"), доступ viewer
подтверждён как read-only (нет кнопок изменения, прямой заход на
/zones/create отдаёт 403).
This commit is contained in:
2026-08-11 16:54:26 +05:00
parent bb492785a4
commit 467a05542c
29 changed files with 1264 additions and 9 deletions
@@ -0,0 +1,70 @@
<?php
namespace Database\Seeders;
use App\Models\AutomationRule;
use App\Models\Device;
use App\Models\DeviceType;
use App\Models\User;
use App\Models\Zone;
use Illuminate\Database\Seeder;
/**
* Demo data for the growbox zone the first zone implemented on the
* platform. Purely example content so the CRUD screens aren't empty on
* first run; nothing here is special-cased in application code.
*/
class DemoGrowboxSeeder extends Seeder
{
public function run(): void
{
$owner = User::where('email', 'owner@example.com')->firstOrFail();
$zone = Zone::query()->firstOrCreate(
['name' => 'Гроубокс'],
['user_id' => $owner->id, 'description' => 'Демо-зона для проверки платформы']
);
$sensorType = DeviceType::where('code', 'sensor_temp_humidity')->firstOrFail();
$fanType = DeviceType::where('code', 'fan')->firstOrFail();
$sensor = Device::query()->firstOrCreate(
['external_id' => 'sensor-1'],
[
'user_id' => $owner->id,
'zone_id' => $zone->id,
'device_type_id' => $sensorType->id,
'name' => 'Датчик температуры',
'protocol' => 'mqtt',
]
);
$fan = Device::query()->firstOrCreate(
['external_id' => 'fan-1'],
[
'user_id' => $owner->id,
'zone_id' => $zone->id,
'device_type_id' => $fanType->id,
'name' => 'Вентилятор',
'protocol' => 'mqtt',
]
);
AutomationRule::query()->firstOrCreate(
[
'condition_source_device_id' => $sensor->id,
'target_device_id' => $fan->id,
],
[
'user_id' => $owner->id,
'zone_id' => $zone->id,
'condition_sensor_type' => 'temperature',
'condition_operator' => '>',
'condition_value' => 28,
'action_type' => 'turn_on',
'action_params' => [],
'is_active' => true,
]
);
}
}