Browse Source

Add legacy Devices Panel analysis

Analyzed legacy PHP admin panel from /home/user/work/luckfox/mybeacon-legacy/www/my-wifi/panel/

Key findings:
- Legacy used DataTables with universal search across all fields (key feature)
- Inline editing via jEditable (too dangerous, using modals instead)
- No multi-tenant isolation (all devices in one table)
- No authentication in admin panel
- SQL injection vulnerabilities
- Stored MAC addresses as text with semicolon separators

What to keep:
✅ Universal search by MAC, organization, user, simple_id
✅ Compact table view with quick filters
✅ Status badges and indicators
✅ Pagination for large lists

What to remove:
❌ OpenVPN tunnels (not in new requirements)
❌ Inline editing (using modals with validation)
❌ Text-based device relationships (using foreign keys)
❌ Hardcoded URLs in database (using JSONB config)

Document includes:
- Complete DB schema analysis (configs, users, user_devices tables)
- Navigation structure
- Inline editing mechanism explanation
- Search/filter implementation examples
- Security issues identified
- Comparison table: Legacy vs New project
- Implementation priorities for new admin panel
root 1 month ago
parent
commit
a9a3ffbe36
1 changed files with 622 additions and 0 deletions
  1. 622 0
      DEVICES_PANEL.md

+ 622 - 0
DEVICES_PANEL.md

@@ -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 в таблице устройств