DEVICES_PANEL.md 22 KB

Анализ 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 (устройства)

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 (клиенты/организации)

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 (связь)

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

Как работает:

  1. Пользователь кликает на ячейку таблицы (с классом .editable-td)
  2. Ячейка превращается в <input> с кнопками "Ok" и "Cancel"
  3. При нажатии "Ok" отправляется AJAX запрос:

    $.get('savewf.php', {
     p1: value,           // новое значение
     p2: $(this).attr('id'),  // ID ячейки (имя поля)
     p3: $(this).parent('tr').attr('id')  // ID строки (id записи)
    })
    
  4. Backend (savewf.php) выполняет SQL:

    UPDATE `configs` SET `{field_name}` = '{value}' WHERE `id`='{id}'
    
  5. Ячейка возвращается к обычному виду с новым значением

Поиск и фильтрация

Библиотека: DataTables

Функционал:

  • Глобальный поиск по всем колонкам таблицы
  • Пользователь вводит любой текст в поле поиска
  • DataTables фильтрует строки в реальном времени
  • Работает по: MAC, SSID, IP, адресам, времени, флагам

Код:

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=лимит)

Логика создания

// 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 - прямая конкатенация в запросах:

    $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 в каждом файле:

    $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:

# 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):

<!-- 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):

// 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):

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