|
|
@@ -0,0 +1,622 @@
|
|
|
+# Анализ Legacy Devices Panel
|
|
|
+
|
|
|
+## Обзор legacy проекта
|
|
|
+
|
|
|
+Расположение: `/home/user/work/luckfox/mybeacon-legacy/www/my-wifi/panel/`
|
|
|
+
|
|
|
+Технологии:
|
|
|
+- **Backend**: PHP 5.x/7.x + MySQL (MariaDB)
|
|
|
+- **Frontend**: jQuery + DataTables + jEditable
|
|
|
+- **Стиль**: Bootstrap 3 + кастомная тема
|
|
|
+
|
|
|
+## Структура БД (Legacy)
|
|
|
+
|
|
|
+### Таблица `configs` (устройства)
|
|
|
+```sql
|
|
|
+CREATE TABLE `configs` (
|
|
|
+ `id` int(11) PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `time` timestamp DEFAULT current_timestamp(), -- Last seen
|
|
|
+ `mac` varchar(40) UNIQUE NOT NULL,
|
|
|
+ `wf_client_ssid` varchar(60), -- WiFi SSID для подключения
|
|
|
+ `wf_client_psk` varchar(60), -- WiFi пароль
|
|
|
+ `ovpn_flag` tinyint(1), -- OpenVPN включен
|
|
|
+ `ovpn_addr` varchar(60), -- URL для получения VPN конфига
|
|
|
+ `wf_flag` tinyint(1), -- WiFi сбор включен
|
|
|
+ `wf_addr` varchar(60), -- URL для отправки WiFi данных
|
|
|
+ `bt_flag` tinyint(1), -- BLE сбор включен
|
|
|
+ `bt_addr` varchar(60), -- URL для отправки BLE данных
|
|
|
+ `fw_flag` tinyint(1), -- Firmware update включен
|
|
|
+ `fw_addr` varchar(60), -- URL для получения firmware
|
|
|
+ `reboot_flag` tinyint(1), -- Флаг перезагрузки
|
|
|
+ `ip` varchar(40) -- IP адрес устройства
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### Таблица `users` (клиенты/организации)
|
|
|
+```sql
|
|
|
+CREATE TABLE `users` (
|
|
|
+ `id` int(11) PRIMARY KEY AUTO_INCREMENT,
|
|
|
+ `login` text NOT NULL,
|
|
|
+ `password` text NOT NULL,
|
|
|
+ `device` text NOT NULL, -- MAC-адреса через ";" (b8:27:eb:c1:46:0e;cc:2d:e0:ca:9f:7e;)
|
|
|
+ `addTime` timestamp DEFAULT current_timestamp(),
|
|
|
+ `dopinfo` text, -- Доп. информация о клиенте
|
|
|
+ `paidTill` text, -- Текст об оплате
|
|
|
+ `folder` text, -- Папка для файлов (не использовалось)
|
|
|
+ `till` date DEFAULT '2022-05-03', -- Дата оплаты до
|
|
|
+ `dopfolder` varchar(128) -- Доп. папка (не использовалось)
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### Таблица `user_devices` (связь)
|
|
|
+```sql
|
|
|
+CREATE TABLE `user_devices` (
|
|
|
+ `mac` varchar(40),
|
|
|
+ `name` varchar(255),
|
|
|
+ `user_id` int(11),
|
|
|
+ `max1000` tinyint(1), -- Лимит (0=оплачен, 1=лимит 1000)
|
|
|
+ `segment` tinyint(1) -- Для Yandex.Аудитории
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+## Навигация (Legacy)
|
|
|
+
|
|
|
+```
|
|
|
+1. Устройства (index.php) - главная страница, список всех устройств
|
|
|
+2. Добавление (adddev.php) - форма добавления клиента с устройствами
|
|
|
+3. Клиенты (clients.php) - список клиентов
|
|
|
+4. Инструкция (info.php) - документация
|
|
|
+5. VPN (внешняя ссылка) - сканер VPN подключений
|
|
|
+```
|
|
|
+
|
|
|
+## Функционал Devices Panel (index.php)
|
|
|
+
|
|
|
+### Таблица устройств
|
|
|
+
|
|
|
+**Колонки:**
|
|
|
+1. `#` - порядковый номер
|
|
|
+2. `MAC-адрес` - читаемый (не редактируемый)
|
|
|
+3. `время` - last seen (не редактируемый)
|
|
|
+4. `wf_ssid` - **РЕДАКТИРУЕМАЯ** (inline editing)
|
|
|
+5. `wf_psk` - **РЕДАКТИРУЕМАЯ**
|
|
|
+6. `ovpn_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
+7. `ovpn_addr` - **РЕДАКТИРУЕМАЯ**
|
|
|
+8. `wf_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
+9. `wf_addr` - **РЕДАКТИРУЕМАЯ**
|
|
|
+10. `bt_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
+11. `bt_addr` - **РЕДАКТИРУЕМАЯ**
|
|
|
+12. `fw_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
+13. `fw_addr` - **РЕДАКТИРУЕМАЯ**
|
|
|
+14. `reboot_flag` - **РЕДАКТИРУЕМАЯ**
|
|
|
+15. `ip` - **РЕДАКТИРУЕМАЯ**
|
|
|
+
|
|
|
+### Ключевая фича: Inline Editing
|
|
|
+
|
|
|
+**Библиотека**: [jEditable](http://www.appelsiini.net/projects/jeditable)
|
|
|
+
|
|
|
+**Как работает:**
|
|
|
+1. Пользователь кликает на ячейку таблицы (с классом `.editable-td`)
|
|
|
+2. Ячейка превращается в `<input>` с кнопками "Ok" и "Cancel"
|
|
|
+3. При нажатии "Ok" отправляется AJAX запрос:
|
|
|
+ ```javascript
|
|
|
+ $.get('savewf.php', {
|
|
|
+ p1: value, // новое значение
|
|
|
+ p2: $(this).attr('id'), // ID ячейки (имя поля)
|
|
|
+ p3: $(this).parent('tr').attr('id') // ID строки (id записи)
|
|
|
+ })
|
|
|
+ ```
|
|
|
+4. Backend (`savewf.php`) выполняет SQL:
|
|
|
+ ```sql
|
|
|
+ UPDATE `configs` SET `{field_name}` = '{value}' WHERE `id`='{id}'
|
|
|
+ ```
|
|
|
+5. Ячейка возвращается к обычному виду с новым значением
|
|
|
+
|
|
|
+### Поиск и фильтрация
|
|
|
+
|
|
|
+**Библиотека**: [DataTables](https://datatables.net/)
|
|
|
+
|
|
|
+**Функционал:**
|
|
|
+- Глобальный поиск по **всем** колонкам таблицы
|
|
|
+- Пользователь вводит любой текст в поле поиска
|
|
|
+- DataTables фильтрует строки в реальном времени
|
|
|
+- Работает по: MAC, SSID, IP, адресам, времени, флагам
|
|
|
+
|
|
|
+**Код:**
|
|
|
+```javascript
|
|
|
+var editableTable = exampleDatatable.dataTable({
|
|
|
+ order: [[ 1, 'desc' ]], // Сортировка по ID по убыванию
|
|
|
+ columnDefs: [ { orderable: false, targets: [ 0 ] } ]
|
|
|
+});
|
|
|
+$('.dataTables_filter input').attr('placeholder', 'Search');
|
|
|
+```
|
|
|
+
|
|
|
+### Сортировка
|
|
|
+
|
|
|
+- Сортировка по любой колонке (кроме первой)
|
|
|
+- По умолчанию: по ID (времени регистрации) по убыванию
|
|
|
+
|
|
|
+## Функционал Clients Panel (clients.php)
|
|
|
+
|
|
|
+### Таблица клиентов
|
|
|
+
|
|
|
+**Колонки:**
|
|
|
+1. `#` - порядковый номер
|
|
|
+2. `Логин`
|
|
|
+3. `Пароль`
|
|
|
+4. `Устройства` - MAC-адреса через `<br>` (разделены по `;`)
|
|
|
+5. `Оплачено до` - ссылка на редактирование даты
|
|
|
+6. `Информация` - допинфо о клиенте
|
|
|
+7. `Оплата` - текст статуса оплаты
|
|
|
+
|
|
|
+### Особенности
|
|
|
+
|
|
|
+- Список устройств выводится вертикально (`implode('<br>', explode(';', $line['device']))`)
|
|
|
+- Ссылка на редактирование даты: `<a href="tilledit.php?id={id}">{till}</a>`
|
|
|
+- Нет inline editing (в отличие от devices)
|
|
|
+- Только чтение данных
|
|
|
+
|
|
|
+## Функционал Add Device (adddev.php)
|
|
|
+
|
|
|
+### Форма создания клиента
|
|
|
+
|
|
|
+**Поля:**
|
|
|
+1. `Логин` - required
|
|
|
+2. `Пароль` - required
|
|
|
+3. `Список устройств` - textarea, через `;` в конце
|
|
|
+ Пример: `d8:0d:17:5e:07:94;ac:84:c6:42:17:90;`
|
|
|
+4. `Информация о клиенте` - textarea (юрлицо, имя, телефон, email)
|
|
|
+5. `Оплата` - textarea (текст до какого числа оплачено)
|
|
|
+6. `Оплачен` - checkbox (max1000: 0=оплачен, 1=лимит)
|
|
|
+
|
|
|
+### Логика создания
|
|
|
+
|
|
|
+```php
|
|
|
+// 1. Валидация
|
|
|
+if (login == null || password == null || device == null) {
|
|
|
+ error("Поля логин, пароль и устройства не могут быть пустыми");
|
|
|
+}
|
|
|
+
|
|
|
+// 2. Проверка существования логина
|
|
|
+$query = "SELECT * FROM `users` WHERE `login` = '{login}'";
|
|
|
+if (row_count > 0) {
|
|
|
+ error("Логин уже занят");
|
|
|
+}
|
|
|
+
|
|
|
+// 3. Создание пользователя
|
|
|
+INSERT INTO `users` (login, password, device, dopinfo, paidTill, till)
|
|
|
+VALUES ('{login}', '{password}', '{device}', '{dopinfo}', '{paidTill}', CURDATE());
|
|
|
+
|
|
|
+$user_id = mysqli_insert_id();
|
|
|
+
|
|
|
+// 4. Создание связей user_devices
|
|
|
+$macs = explode(";", device);
|
|
|
+foreach ($macs as $mac) {
|
|
|
+ INSERT INTO `user_devices` (mac, name, user_id, max1000, segment)
|
|
|
+ VALUES ('{mac}', '{mac}', {user_id}, {max1000}, 0);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Проблемы Legacy кода
|
|
|
+
|
|
|
+### Безопасность
|
|
|
+
|
|
|
+1. **SQL Injection** - прямая конкатенация в запросах:
|
|
|
+ ```php
|
|
|
+ $query = "UPDATE `configs` SET `".$_REQUEST['p2']."` = '".$_REQUEST['p1']."' WHERE `id`='".$_REQUEST['p3']."'";
|
|
|
+ ```
|
|
|
+2. **XSS** - нет экранирования вывода
|
|
|
+3. **Пароли в открытом виде** - хранятся как plain text
|
|
|
+4. **Нет CSRF защиты**
|
|
|
+5. **Нет валидации типов данных**
|
|
|
+
|
|
|
+### Архитектура
|
|
|
+
|
|
|
+1. **Нет разделения ролей** - все пользователи видят всё
|
|
|
+2. **Нет multi-tenant изоляции** - одна таблица на всех
|
|
|
+3. **Нет аутентификации в админке** - доступ без проверки
|
|
|
+4. **Хардкод credentials** в каждом файле:
|
|
|
+ ```php
|
|
|
+ $dbname = 'wifi';
|
|
|
+ $dbuser = 'p328882_wifi';
|
|
|
+ $dbpass = '0354c0598ld';
|
|
|
+ ```
|
|
|
+5. **Связь many-to-many через текст** - MAC-адреса через `;`
|
|
|
+
|
|
|
+### UX
|
|
|
+
|
|
|
+1. **Inline editing опасен** - легко случайно изменить данные
|
|
|
+2. **Нет подтверждения удаления**
|
|
|
+3. **Нет истории изменений** (audit logs)
|
|
|
+4. **Нет валидации MAC-адресов**
|
|
|
+5. **Нет статусов устройств** (online/offline/error)
|
|
|
+
|
|
|
+## Анализ требований для нового проекта
|
|
|
+
|
|
|
+### Что взять из Legacy
|
|
|
+
|
|
|
+#### 1. **Универсальный поиск по всем полям** ⭐ КЛЮЧЕВАЯ ФИЧА
|
|
|
+
|
|
|
+**Почему важно:**
|
|
|
+- Быстро найти устройство по MAC
|
|
|
+- Найти устройства организации
|
|
|
+- Найти по IP адресу
|
|
|
+- Найти по owner/admin email
|
|
|
+
|
|
|
+**Реализация в новом проекте:**
|
|
|
+
|
|
|
+**Backend:**
|
|
|
+```python
|
|
|
+# backend/app/api/v1/superadmin/devices.py или client/devices.py
|
|
|
+@router.get("/devices", response_model=DeviceListResponse)
|
|
|
+async def get_devices(
|
|
|
+ search: Optional[str] = None,
|
|
|
+ organization_id: Optional[int] = None,
|
|
|
+ offset: int = 0,
|
|
|
+ limit: int = 50,
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user)
|
|
|
+):
|
|
|
+ query = select(Device)
|
|
|
+
|
|
|
+ # Фильтр по организации (для multi-tenant)
|
|
|
+ if current_user.role != "superadmin":
|
|
|
+ query = query.where(Device.organization_id == current_user.organization_id)
|
|
|
+ elif organization_id:
|
|
|
+ query = query.where(Device.organization_id == organization_id)
|
|
|
+
|
|
|
+ # Универсальный поиск
|
|
|
+ if search:
|
|
|
+ search_filter = or_(
|
|
|
+ Device.mac_address.ilike(f"%{search}%"),
|
|
|
+ Device.simple_id.cast(String).ilike(f"%{search}%"),
|
|
|
+ Organization.name.ilike(f"%{search}%"),
|
|
|
+ User.email.ilike(f"%{search}%"),
|
|
|
+ User.full_name.ilike(f"%{search}%")
|
|
|
+ )
|
|
|
+ query = (
|
|
|
+ query
|
|
|
+ .outerjoin(Organization)
|
|
|
+ .outerjoin(User, User.organization_id == Device.organization_id)
|
|
|
+ .where(search_filter)
|
|
|
+ )
|
|
|
+
|
|
|
+ total = await db.scalar(select(func.count()).select_from(query.subquery()))
|
|
|
+ devices = await db.scalars(query.offset(offset).limit(limit))
|
|
|
+
|
|
|
+ return DeviceListResponse(devices=devices.all(), total=total)
|
|
|
+```
|
|
|
+
|
|
|
+**Frontend (Vue 3):**
|
|
|
+```vue
|
|
|
+<!-- frontend/src/views/superadmin/DevicesView.vue -->
|
|
|
+<template>
|
|
|
+ <div class="page">
|
|
|
+ <div class="page-header">
|
|
|
+ <h1>{{ $t('devices.title') }}</h1>
|
|
|
+
|
|
|
+ <!-- Поле поиска -->
|
|
|
+ <div class="search-box">
|
|
|
+ <input
|
|
|
+ v-model="searchQuery"
|
|
|
+ @input="debouncedSearch"
|
|
|
+ :placeholder="$t('devices.searchPlaceholder')"
|
|
|
+ type="text"
|
|
|
+ />
|
|
|
+ <i class="search-icon">🔍</i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <table class="data-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>{{ $t('devices.simpleId') }}</th>
|
|
|
+ <th>{{ $t('devices.macAddress') }}</th>
|
|
|
+ <th>{{ $t('devices.organization') }}</th>
|
|
|
+ <th>{{ $t('common.status') }}</th>
|
|
|
+ <th>{{ $t('devices.lastSeen') }}</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="device in devices" :key="device.id">
|
|
|
+ <td>#{{ device.simple_id }}</td>
|
|
|
+ <td><code>{{ device.mac_address }}</code></td>
|
|
|
+ <td>{{ getOrganizationName(device.organization_id) }}</td>
|
|
|
+ <td><span class="badge" :class="`status-${device.status}`">{{ device.status }}</span></td>
|
|
|
+ <td>{{ formatDate(device.last_seen_at) }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+
|
|
|
+ <!-- Пагинация -->
|
|
|
+ <div class="pagination">
|
|
|
+ <button @click="prevPage" :disabled="offset === 0">Prev</button>
|
|
|
+ <span>{{ currentPage }} / {{ totalPages }}</span>
|
|
|
+ <button @click="nextPage" :disabled="!hasMore">Next</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed } from 'vue'
|
|
|
+import { debounce } from 'lodash-es'
|
|
|
+import devicesApi from '@/api/devices'
|
|
|
+
|
|
|
+const devices = ref([])
|
|
|
+const searchQuery = ref('')
|
|
|
+const offset = ref(0)
|
|
|
+const limit = ref(50)
|
|
|
+const total = ref(0)
|
|
|
+
|
|
|
+const hasMore = computed(() => offset.value + limit.value < total.value)
|
|
|
+const totalPages = computed(() => Math.ceil(total.value / limit.value))
|
|
|
+const currentPage = computed(() => Math.floor(offset.value / limit.value) + 1)
|
|
|
+
|
|
|
+async function loadDevices() {
|
|
|
+ try {
|
|
|
+ const response = await devicesApi.getAllSuperadmin({
|
|
|
+ search: searchQuery.value,
|
|
|
+ offset: offset.value,
|
|
|
+ limit: limit.value
|
|
|
+ })
|
|
|
+ devices.value = response.devices
|
|
|
+ total.value = response.total
|
|
|
+ } catch (err) {
|
|
|
+ console.error(err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const debouncedSearch = debounce(() => {
|
|
|
+ offset.value = 0 // Reset to first page
|
|
|
+ loadDevices()
|
|
|
+}, 300) // 300ms delay
|
|
|
+
|
|
|
+function nextPage() {
|
|
|
+ offset.value += limit.value
|
|
|
+ loadDevices()
|
|
|
+}
|
|
|
+
|
|
|
+function prevPage() {
|
|
|
+ offset.value = Math.max(0, offset.value - limit.value)
|
|
|
+ loadDevices()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(loadDevices)
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+**Переводы (i18n):**
|
|
|
+```javascript
|
|
|
+// frontend/src/i18n/index.js
|
|
|
+const messages = {
|
|
|
+ ru: {
|
|
|
+ devices: {
|
|
|
+ searchPlaceholder: 'Поиск по MAC, организации, владельцу...'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ en: {
|
|
|
+ devices: {
|
|
|
+ searchPlaceholder: 'Search by MAC, organization, owner...'
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 2. **Быстрый просмотр в таблице**
|
|
|
+
|
|
|
+- Компактная таблица с основной информацией
|
|
|
+- Сортировка по любой колонке
|
|
|
+- Pagination для больших списков
|
|
|
+
|
|
|
+#### 3. **Inline индикаторы**
|
|
|
+
|
|
|
+- Статус устройства (online/offline/error) - цветные badges
|
|
|
+- Дата last_seen с человекочитаемым форматом
|
|
|
+- Organization name вместо ID
|
|
|
+
|
|
|
+### Что НЕ брать из Legacy
|
|
|
+
|
|
|
+#### 1. **Inline Editing** ❌
|
|
|
+
|
|
|
+**Проблемы:**
|
|
|
+- Случайные изменения (кликнул мимо)
|
|
|
+- Нет валидации
|
|
|
+- Нет подтверждения
|
|
|
+- Нет истории изменений
|
|
|
+
|
|
|
+**Решение:** Модальные окна для редактирования (как сейчас)
|
|
|
+
|
|
|
+#### 2. **Текстовые связи (MAC через `;`)** ❌
|
|
|
+
|
|
|
+**Проблема:** `device` text NOT NULL - "b8:27:eb:c1:46:0e;cc:2d:e0:ca:9f:7e;"
|
|
|
+
|
|
|
+**Решение:** Нормализованная БД с foreign keys (как сейчас)
|
|
|
+
|
|
|
+#### 3. **OpenVPN туннели** ❌
|
|
|
+
|
|
|
+**Из ТЗ:** "Сейчас у нас VPN не будет"
|
|
|
+
|
|
|
+**Убираем:**
|
|
|
+- `ovpn_flag`
|
|
|
+- `ovpn_addr`
|
|
|
+- VPN страница в навигации
|
|
|
+
|
|
|
+#### 4. **Множественные флаги и адреса** ❌
|
|
|
+
|
|
|
+**Legacy имело:**
|
|
|
+- `wf_flag` + `wf_addr` (WiFi)
|
|
|
+- `bt_flag` + `bt_addr` (BLE)
|
|
|
+- `fw_flag` + `fw_addr` (Firmware)
|
|
|
+
|
|
|
+**Новый подход:**
|
|
|
+- Модульность на уровне Organization: `wifi_enabled`, `ble_enabled`
|
|
|
+- Единый API endpoint вместо хардкод URL в БД
|
|
|
+- Config как JSONB для гибкости
|
|
|
+
|
|
|
+#### 5. **Inline WiFi credentials** ❌
|
|
|
+
|
|
|
+**Legacy:** `wf_client_ssid` и `wf_client_psk` в таблице configs
|
|
|
+
|
|
|
+**Проблема:**
|
|
|
+- Пароли в открытом виде
|
|
|
+- Одинаковые для всех устройств организации
|
|
|
+
|
|
|
+**Решение:**
|
|
|
+- Credentials на уровне Organization (в зашифрованном виде или в vault)
|
|
|
+- Device получает через защищенный API
|
|
|
+
|
|
|
+## Новая структура Devices Panel
|
|
|
+
|
|
|
+### Фильтры (обязательно)
|
|
|
+
|
|
|
+**Для новой админки:**
|
|
|
+
|
|
|
+1. **Глобальный поиск** (текстовое поле)
|
|
|
+ - По MAC-адресу
|
|
|
+ - По simple_id (#1, #2, #3)
|
|
|
+ - По имени организации
|
|
|
+ - По email владельца организации
|
|
|
+
|
|
|
+2. **Фильтр по организации** (dropdown для superadmin)
|
|
|
+ - "Все организации"
|
|
|
+ - "Не назначены"
|
|
|
+ - Список организаций
|
|
|
+
|
|
|
+3. **Фильтр по статусу** (chips/badges)
|
|
|
+ - Online
|
|
|
+ - Offline
|
|
|
+ - Error
|
|
|
+
|
|
|
+4. **Сортировка**
|
|
|
+ - По Simple ID (по умолчанию)
|
|
|
+ - По дате последнего подключения
|
|
|
+ - По организации
|
|
|
+ - По статусу
|
|
|
+
|
|
|
+### Таблица устройств
|
|
|
+
|
|
|
+**Колонки (минимум):**
|
|
|
+
|
|
|
+| Колонка | Описание | Для Superadmin | Для Client |
|
|
|
+|---------|----------|----------------|------------|
|
|
|
+| Simple ID | #1, #2, #3 | ✅ | ✅ |
|
|
|
+| MAC Address | `ac:84:c6:d4:9c:c4` | ✅ | ✅ |
|
|
|
+| Organization | Название организации | ✅ | ❌ (скрыта) |
|
|
|
+| Owner | Email владельца | ✅ | ✅ |
|
|
|
+| Status | online/offline/error | ✅ | ✅ |
|
|
|
+| Last Seen | "2 минуты назад" | ✅ | ✅ |
|
|
|
+| Actions | Edit, Delete | ✅ | Owner/Admin only |
|
|
|
+
|
|
|
+### Детали устройства (модальное окно или отдельная страница)
|
|
|
+
|
|
|
+**Общая информация:**
|
|
|
+- Simple ID
|
|
|
+- MAC Address
|
|
|
+- Organization
|
|
|
+- Status
|
|
|
+- Created At
|
|
|
+- Last Seen At
|
|
|
+- IP Address (если онлайн)
|
|
|
+
|
|
|
+**Конфигурация (JSONB):**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "wifi_enabled": true,
|
|
|
+ "ble_enabled": true,
|
|
|
+ "upload_interval": 300,
|
|
|
+ "scan_interval": 60,
|
|
|
+ "custom_settings": {}
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**История подключений:**
|
|
|
+- Таблица последних 10 подключений
|
|
|
+- Timestamp, IP, Duration
|
|
|
+
|
|
|
+**Действия:**
|
|
|
+- Изменить организацию (superadmin)
|
|
|
+- Изменить статус
|
|
|
+- Удалить устройство (с подтверждением)
|
|
|
+- Просмотр audit logs
|
|
|
+
|
|
|
+## Сравнение Legacy vs Новый проект
|
|
|
+
|
|
|
+| Функция | Legacy | Новый проект |
|
|
|
+|---------|--------|--------------|
|
|
|
+| **Аутентификация** | ❌ Нет | ✅ JWT (access + refresh) |
|
|
|
+| **Multi-tenant** | ❌ Все в одной куче | ✅ Изоляция по organization_id |
|
|
|
+| **Роли** | ❌ Нет | ✅ 6 ролей (superadmin, owner, admin, manager, operator, viewer) |
|
|
|
+| **Поиск** | ✅ По всем полям | ✅ По MAC, org, user, simple_id |
|
|
|
+| **Редактирование** | ✅ Inline (опасно) | ✅ Модальные окна с валидацией |
|
|
|
+| **Simple ID** | ❌ Нет | ✅ Auto-increment (#1, #2, #3) |
|
|
|
+| **OpenVPN** | ✅ Было | ❌ Убрали |
|
|
|
+| **Audit Logs** | ❌ Нет | ✅ Полное логирование |
|
|
|
+| **Статусы** | ❌ Нет | ✅ online/offline/error |
|
|
|
+| **Пагинация** | ✅ DataTables | ✅ Backend pagination |
|
|
|
+| **i18n** | ❌ Только русский | ✅ RU/EN |
|
|
|
+
|
|
|
+## Приоритеты реализации
|
|
|
+
|
|
|
+### Шаг 1: Базовый функционал (уже есть)
|
|
|
+- ✅ CRUD устройств
|
|
|
+- ✅ Фильтр по организации
|
|
|
+- ✅ Статусы
|
|
|
+- ✅ Last seen
|
|
|
+
|
|
|
+### Шаг 2: Поиск (TODO)
|
|
|
+- [ ] Универсальное поле поиска
|
|
|
+- [ ] Backend endpoint с фильтрацией
|
|
|
+- [ ] Debounce для поиска
|
|
|
+- [ ] Подсветка результатов
|
|
|
+
|
|
|
+### Шаг 3: Детали устройства (TODO)
|
|
|
+- [ ] Модальное окно с полной информацией
|
|
|
+- [ ] История подключений
|
|
|
+- [ ] Редактирование конфига (JSONB)
|
|
|
+- [ ] Просмотр audit logs для устройства
|
|
|
+
|
|
|
+### Шаг 4: Улучшения UX (TODO)
|
|
|
+- [ ] Сортировка таблицы
|
|
|
+- [ ] Фильтр по статусу (chips)
|
|
|
+- [ ] Экспорт в CSV/Excel
|
|
|
+- [ ] Bulk actions (массовое удаление/изменение)
|
|
|
+
|
|
|
+## Выводы
|
|
|
+
|
|
|
+### Ключевые изменения от Legacy
|
|
|
+
|
|
|
+1. **Безопасность на первом месте:**
|
|
|
+ - JWT аутентификация
|
|
|
+ - RBAC система прав
|
|
|
+ - SQL injection защита (SQLAlchemy)
|
|
|
+ - Audit logging
|
|
|
+
|
|
|
+2. **Multi-tenant архитектура:**
|
|
|
+ - Изоляция данных по организациям
|
|
|
+ - Разные уровни доступа
|
|
|
+
|
|
|
+3. **UX улучшения:**
|
|
|
+ - Simple ID вместо длинных MAC
|
|
|
+ - Модальные окна вместо inline edit
|
|
|
+ - Подтверждение опасных действий
|
|
|
+ - i18n поддержка
|
|
|
+
|
|
|
+4. **Современный стек:**
|
|
|
+ - FastAPI вместо PHP
|
|
|
+ - Vue 3 вместо jQuery
|
|
|
+ - PostgreSQL вместо MySQL
|
|
|
+ - REST API вместо прямых SQL запросов
|
|
|
+
|
|
|
+### Что сохранили
|
|
|
+
|
|
|
+1. **Универсальный поиск** - ключевая фича для удобства
|
|
|
+2. **Компактная таблица** - быстрый просмотр списка
|
|
|
+3. **Статусы и индикаторы** - визуальная обратная связь
|
|
|
+4. **Пагинация** - для больших списков
|
|
|
+
|
|
|
+### Что убрали
|
|
|
+
|
|
|
+1. OpenVPN туннели (по ТЗ)
|
|
|
+2. Inline editing (небезопасно)
|
|
|
+3. Текстовые связи через `;`
|
|
|
+4. Хардкод URL в БД
|
|
|
+5. WiFi credentials в таблице устройств
|