diff --git a/debian/gonets/usr/share/gonets/SRV/.gitignore b/.gitignore
similarity index 55%
rename from debian/gonets/usr/share/gonets/SRV/.gitignore
rename to .gitignore
index b527eea..e92ccd0 100644
--- a/debian/gonets/usr/share/gonets/SRV/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@ __pycache__
.wheels
.vendor
.python3-deb
+SRV/storage/subnets/*
+SRV/storage/wifi/wifi.json
+build/
diff --git a/.pc/.quilt_patches b/.pc/.quilt_patches
deleted file mode 100644
index 6857a8d..0000000
--- a/.pc/.quilt_patches
+++ /dev/null
@@ -1 +0,0 @@
-debian/patches
diff --git a/.pc/.quilt_series b/.pc/.quilt_series
deleted file mode 100644
index c206706..0000000
--- a/.pc/.quilt_series
+++ /dev/null
@@ -1 +0,0 @@
-series
diff --git a/.pc/.version b/.pc/.version
deleted file mode 100644
index 0cfbf08..0000000
--- a/.pc/.version
+++ /dev/null
@@ -1 +0,0 @@
-2
diff --git a/.pc/1/SRV/services/ip_service.py b/.pc/1/SRV/services/ip_service.py
deleted file mode 100644
index 84c3cca..0000000
--- a/.pc/1/SRV/services/ip_service.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import asyncio
-from typing import Dict, Any
-from sdbus_async.networkmanager import (
- NetworkManager,
- NetworkManagerSettings,
- NetworkConnectionSettings,
- NetworkDeviceGeneric,
-)
-import sdbus
-from sdbus_async.networkmanager.settings import ConnectionProfile
-from services.arp_service import NetworkScanner
-
-class IPService:
- """
- Сервис проверки доступности IP-адреса и применения настроек сети
- """
- def __init__(self):
- self.arp_scanner = NetworkScanner()
- self.bus = sdbus.sd_bus_open_system()
- sdbus.set_default_bus(self.bus)
-
- def _reset_bus(self):
- """ Перезапускает соединение с системной шиной D-Bus """
- new_bus = sdbus.sd_bus_open_system()
- sdbus.set_default_bus(new_bus)
- if hasattr(self, 'bus') and self.bus is not None:
- self.bus = None
- self.bus = new_bus
-
- async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]:
- """ Проверяет доступность IP-адреса перед его применением """
- self._reset_bus()
- clients = self.arp_scanner.scan_network()
- new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
- if any(client['ip'] == new_ip for client in clients):
- return {"available": False, "message": f"IP address {new_ip} уже занят."}
- return {"available": True, "message": "Network profile settings are available for use."}
-
- async def apply_ip_config(self, args: Dict[str, Any]) -> None:
- """ Применяет проверенные настройки IP """
- self._reset_bus()
- nm = NetworkManager()
- settings = NetworkManagerSettings()
- connection_paths = await settings.get_connections_by_id(args["connection_id"])
-
- if not connection_paths:
- raise ValueError(f"Подключение {args['connection_id']} не найдено")
-
- connection = NetworkConnectionSettings(connection_paths[0])
- profile = await connection.get_profile()
- settings_dict = profile.to_settings_dict()
-
- for key_path, value in args["ipv4_updates"].items():
- section, *props = key_path.split('.')
- current = settings_dict[section]
- for prop in props[:-1]:
- current = current.setdefault(prop, {})
- current[props[-1]] = value
-
- new_profile = ConnectionProfile.from_settings_dict(settings_dict)
- await connection.update(new_profile.to_dbus())
- print(f"Обновлены настройки для {args['connection_id']}:")
-
- interface_name = settings_dict['connection']['interface-name']
- devices = await nm.get_devices()
- for device_path in devices:
- device = NetworkDeviceGeneric(device_path)
- if await device.interface == interface_name:
- await device.disconnect()
- print(f"Отключён от {interface_name}")
- await nm.activate_connection(connection=connection_paths[0], device=device_path)
- print(f"Переподключён к {args['connection_id']}")
- return
-
- raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено")
diff --git a/.pc/2/SRV/services/ip_service.py b/.pc/2/SRV/services/ip_service.py
deleted file mode 100644
index b0c35af..0000000
--- a/.pc/2/SRV/services/ip_service.py
+++ /dev/null
@@ -1,112 +0,0 @@
-from sdbus_async.networkmanager import NetworkManager, NetworkManagerSettings, NetworkConnectionSettings, NetworkDeviceGeneric, ActiveConnection
-import asyncio
-from sdbus_async.networkmanager.settings import ConnectionProfile
-from typing import Dict, Any
-import sdbus
-from services.arp_service import NetworkScanner
-
-class IPService:
- """
- Сервис проверки доступности IP-адреса и применения настроек сети
- """
- def __init__(self):
- self.arp_scanner = NetworkScanner()
- self.bus = sdbus.sd_bus_open_system()
- sdbus.set_default_bus(self.bus)
-
- def _reset_bus(self):
- """ Перезапускает соединение с системной шиной D-Bus """
- new_bus = sdbus.sd_bus_open_system()
- sdbus.set_default_bus(new_bus)
- if hasattr(self, 'bus') and self.bus is not None:
- self.bus = None
- self.bus = new_bus
-
- async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]:
- """ Проверяет доступность IP-адреса перед его применением """
- self._reset_bus()
- clients = self.arp_scanner.scan_network()
- new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
- if any(client['ip'] == new_ip for client in clients):
- return {"available": False, "message": f"IP address {new_ip} уже доступен."}
- return {"available": True, "message": "Network profile settings are available for use."}
-
- async def apply_ip_config(self, args: Dict[str, Any]) -> None:
- """ Применяет проверенные настройки IP """
- self._reset_bus()
- nm = NetworkManager(self.bus)
- settings = NetworkManagerSettings(self.bus)
- connection_paths = await settings.get_connections_by_id(args["connection_id"])
-
- if not connection_paths:
- raise ValueError(f"Подключение {args['connection_id']} не найдено")
-
- connection = NetworkConnectionSettings(connection_paths[0], self.bus)
- profile = await connection.get_profile()
- settings_dict = profile.to_settings_dict()
-
- # Обновляем настройки IPv4
- for key_path, value in args["ipv4_updates"].items():
- section, *props = key_path.split('.')
- current = settings_dict[section]
- for prop in props[:-1]:
- current = current.setdefault(prop, {})
- current[props[-1]] = value
-
- new_profile = ConnectionProfile.from_settings_dict(settings_dict)
- await connection.update(new_profile.to_dbus())
- print(f"Обновлены настройки для {args['connection_id']}:")
-
- # Проверяем interface-name
- interface_name = settings_dict['connection'].get('interface-name')
- if not interface_name:
- # Ищем устройство, связанное с connection_id через активное соединение
- devices = await nm.get_devices()
- for device_path in devices:
- device = NetworkDeviceGeneric(device_path, self.bus)
- active_connection = await device.active_connection
- if active_connection != '/':
- active_conn = ActiveConnection(active_connection, self.bus)
- conn_path = await active_conn.connection # Путь к настройкам соединения
- if conn_path != '/':
- conn_settings = NetworkConnectionSettings(conn_path, self.bus)
- active_profile = await conn_settings.get_profile()
- if active_profile.to_settings_dict()['connection']['id'] == args["connection_id"]:
- interface_name = await device.interface
- settings_dict['connection']['interface-name'] = interface_name
- new_profile = ConnectionProfile.from_settings_dict(settings_dict)
- await connection.update(new_profile.to_dbus())
- print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
- break
-
- if not interface_name:
- # Если не нашли активное соединение, пробуем получить интерфейс из настроек устройства
- devices = await nm.get_devices()
- for device_path in devices:
- device = NetworkDeviceGeneric(device_path, self.bus)
- applied_connection = await device.get_applied_connection()
- connection_settings = applied_connection[0]
- conn_id = connection_settings.get('connection', {}).get('id', ('s', ''))[1]
- if conn_id == args["connection_id"]:
- interface_name = await device.interface
- settings_dict['connection']['interface-name'] = interface_name
- new_profile = ConnectionProfile.from_settings_dict(settings_dict)
- await connection.update(new_profile.to_dbus())
- print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
- break
-
- if not interface_name:
- raise ValueError(f"Не удалось определить interface-name для {args['connection_id']}")
-
- # Переподключаем устройство
- devices = await nm.get_devices()
- for device_path in devices:
- device = NetworkDeviceGeneric(device_path, self.bus)
- if await device.interface == interface_name:
- await device.disconnect()
- print(f"Отключён от {interface_name}")
- await nm.activate_connection(connection=connection_paths[0], device=device_path)
- print(f"Переподключён к {args['connection_id']}")
- return
-
- raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено")
\ No newline at end of file
diff --git a/.pc/applied-patches b/.pc/applied-patches
deleted file mode 100644
index 1191247..0000000
--- a/.pc/applied-patches
+++ /dev/null
@@ -1,2 +0,0 @@
-1
-2
diff --git a/.woodpecker/.woodpecker.yml b/.woodpecker/.woodpecker.yml
new file mode 100644
index 0000000..6220c93
--- /dev/null
+++ b/.woodpecker/.woodpecker.yml
@@ -0,0 +1,137 @@
+when:
+ event: manual
+
+steps:
+ checkout:
+ image: debian:latest
+ commands:
+ # ПОДГОТОВКА ГИТА И КЛОНИРОВАНИЕ GONETS
+ - apt update
+ - apt install -y git git-lfs
+ - git lfs install
+
+ # УСТАНОВКА НЕОБХОДИМЫХ ЗАВИСИМОСТЕЙ НА СИСТЕМЕ
+ - apt update
+ - apt install -y python3 curl findutils grep sed openssh-server tree libnss3 libnss3-dev libnspr4 libnspr4-dev libasound2 libasound2-dev python3.11-venv python3-pip python3-poetry nodejs npm debhelper devscripts build-essential fakeroot dh-make dpkg-dev cmake libgirepository1.0-dev gobject-introspection libcairo2-dev pkg-config python3-dev libdbus-1-dev
+ - python3 --version
+ - node -v
+ - npm -v
+ raspberry
+
+ # ПОДГОТОВКА СПИСКА PYTHON ЗАВИСИМОСТЕЙ GONETS
+ - rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED
+ - rm -rf /root/.local/share/pypoetry
+ - rm -rf /root/.poetry
+ - rm -rf /usr/bin/poetry
+ - pip install pipx
+ - pipx ensurepath
+ - pipx install poetry
+ - echo 'export PATH="$HOME/.local/bin:$PATH"' >> /root/.bashrc
+ - . /root/.bashrc
+ - poetry --version
+ - cd SRV
+ - poetry env activate
+ - poetry add poetry-plugin-export
+ - poetry install
+ - poetry self update
+ - rm -f requirements.txt
+ - poetry run poetry export -f requirements.txt --output requirements.txt --without-hashes
+
+ # ПОДГОТОВКА И СБОРКА FRONTEND ЧАСТИ GONETS
+ - pwd
+ - cd ..
+ - pwd
+ - cd FRONT/DEV
+ - pwd
+ - npm install
+ - npm run package
+ - cd dist/gonets-linux-x64/resources/app
+ - pwd
+ - npm run build
+ - sed -i 's#href="/assets/#href="./assets/#g' dist/index.html
+ - sed -i 's#src="/assets/#src="./assets/#g' dist/index.html
+ - cd ../../../../..
+ - pwd
+ - rm -rf BUILD/*
+ - cp -r DEV/dist/gonets-linux-x64/* BUILD/
+ - rm -rf DEV
+
+ # ИНИЦИАЛИЗАЦИЯ DEBIAN КОНФИГУРАЦИЙ ДЛЯ СБОРКИ
+ - cd ..
+ - pwd
+ - cd ..
+ - pwd
+ - mv gonets gonets-0.1
+ - cd gonets-0.1
+ - pwd
+ - ls
+ - DEBEMAIL="devteam@gossys.dev" DEBFULLNAME="GOSSYS LLC" dh_make --single -y --createorig
+ - cd debian/
+ - rm -f *.ex
+ - rm -f README.*
+
+ # КЛОНИРОВАНИЕ НЕОБХОДИМЫХ ШАБЛОНОВ КОНФИГОВ ДЕБИАНА ДЛЯ СБОРКИ
+ - mkdir -p /woodpecker/src/dev.gossys.dev/HybrydMain/templates
+ - git clone https://s.galanskiy:${GONETS}@dev.gossys.dev/s.galanskiy/gonets-debian-template.git /woodpecker/src/dev.gossys.dev/HybrydMain/templates
+ - mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/changelog /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
+ - mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/control /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
+ - mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/copyright /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
+ - mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/install /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
+ - mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/postinst /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
+ - mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/postrm /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
+ - mv /woodpecker/src/dev.gossys.dev/HybrydMain/templates/rules /woodpecker/src/dev.gossys.dev/HybrydMain/gonets-0.1/debian/
+
+ # СБОРКА ДЕБИАН ПАКЕТА
+ - cd ..
+ - pwd
+ - dpkg-buildpackage -us -uc -b
+
+ # ИЗВЛЕЧЕНИЕ ВЕРСИИ ПАКЕТА ДЛЯ КОММИТА
+ - VERSION=$(dpkg-parsechangelog -l./debian/changelog --show-field Version)
+ - echo "VERSION=$VERSION" > version.env
+
+ # КОММИТ И ТЕГ НА ВЕТКУ DEVELOP
+ - . ./version.env
+ - git config user.name "s.galanskiy"
+ - git config user.email "s.galanskiy@gossys.dev"
+ - git fetch --all --prune
+ - git branch -r
+ - git checkout -b "release/$VERSION"
+ - git commit -am "Релиз $VERSION"
+ - git tag -f "v$VERSION"
+ - git push "https://s.galanskiy:b4785198d18200dfed812dc4299575f736255bd7@dev.gossys.dev/HybrydMain/gonets.git" "release/$VERSION"
+ - git push -f https://s.galanskiy:b4785198d18200dfed812dc4299575f736255bd7@dev.gossys.dev/HybrydMain/gonets.git "v$VERSION"
+
+ # СОЗДАНИЕ И ЗАГРУЗКА РЕЛИЗА ЧЕРЕЗ API
+ - |
+ VERSION=$(dpkg-parsechangelog -l./debian/changelog --show-field Version)
+ echo "VERSION=$VERSION" > version.env
+
+ # Задать переменные
+ . ./version.env
+ TAG="v$VERSION"
+
+ DEB_PATH=$(find .. -maxdepth 1 -type f -name "gonets_*_amd64.deb" | head -n 1)
+
+ if [ ! -f "$DEB_PATH" ]; then
+ echo ".deb файл не найден по пути: $DEB_PATH"
+ exit 1
+ fi
+
+ RELEASE_JSON=$(curl -s -X POST "https://dev.gossys.dev/api/v1/repos/HybrydMain/gonets/releases" \
+ -H "Authorization: token b4785198d18200dfed812dc4299575f736255bd7" \
+ -H "Content-Type: application/json" \
+ -d "{\"tag_name\": \"$TAG\", \"name\": \"Релиз $TAG\", \"body\": \"Сборка .deb пакета для версии $TAG\", \"draft\": false, \"prerelease\": true}")
+
+ RELEASE_ID=$(echo "$RELEASE_JSON" | grep '"id":' | head -n1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
+
+ if [ -z "$RELEASE_ID" ]; then
+ echo "Не удалось получить ID релиза из ответа:"
+ echo "$RELEASE_JSON"
+ exit 1
+ fi
+
+ curl -s -X POST \
+ -H "Authorization: token b4785198d18200dfed812dc4299575f736255bd7" \
+ -F "attachment=@$DEB_PATH" \
+ "https://dev.gossys.dev/api/v1/repos/HybrydMain/gonets/releases/$RELEASE_ID/assets" || echo "Ошибка загрузки файла"
\ No newline at end of file
diff --git a/FRONT/BUILD/index.html b/FRONT/BUILD/index.html
new file mode 100644
index 0000000..8f4d4d1
--- /dev/null
+++ b/FRONT/BUILD/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ GoNets
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FRONT/BUILD/resources/app/index.tsx b/FRONT/BUILD/resources/app/index.tsx
index 38c8de5..eeb0635 100644
--- a/FRONT/BUILD/resources/app/index.tsx
+++ b/FRONT/BUILD/resources/app/index.tsx
@@ -1,10 +1,8 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
-import { NetworkService } from './src/api/request.ts';
import { useIpStore } from './src/store';
import { useEffect } from 'react';
-import { useAdminStore } from './src/store/adminStore.ts';
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
import {
diff --git a/FRONT/BUILD/resources/app/main.js b/FRONT/BUILD/resources/app/main.js
index 4c3bd59..df09e91 100644
--- a/FRONT/BUILD/resources/app/main.js
+++ b/FRONT/BUILD/resources/app/main.js
@@ -21,7 +21,7 @@ function createWindow() {
win.show();
if (process.env.NODE_ENV === 'development') {
- win.loadURL('http://localhost:3000');
+ win.loadURL('https://localhost:8001');
win.webContents.openDevTools();
} else {
win.loadFile(path.join(__dirname, 'dist/index.html'));
diff --git a/FRONT/BUILD/resources/app/package.json b/FRONT/BUILD/resources/app/package.json
index 0873f2e..304540a 100644
--- a/FRONT/BUILD/resources/app/package.json
+++ b/FRONT/BUILD/resources/app/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@iconify/icons-ic": "^1.2.13",
+ "@iconify/icons-ph": "^1.2.5",
"@iconify/react": "^5.2.0",
"@reduxjs/toolkit": "^2.6.0",
"@xyflow/react": "^12.3.5",
diff --git a/FRONT/BUILD/resources/app/src/api/request.ts b/FRONT/BUILD/resources/app/src/api/request.ts
index 377d522..27dafd1 100644
--- a/FRONT/BUILD/resources/app/src/api/request.ts
+++ b/FRONT/BUILD/resources/app/src/api/request.ts
@@ -82,4 +82,79 @@ export class NetworkService {
return error.response;
}
}
+
+ static async scanUser(ipAddress: string): Promise {
+ try {
+ const axiosInstance = this.createAxiosInstance(ipAddress);
+ return await axiosInstance.get('/subnet/devices');
+ } catch (error: any) {
+ return error.response;
+ }
+ }
+
+ static async scanWifi(ipAddress: string): Promise {
+ try {
+ const axiosInstance = this.createAxiosInstance(ipAddress);
+ return await axiosInstance.get('/subnet/wifi');
+ } catch (error: any) {
+ return error.response;
+ }
+ }
+ static async addNote(ipAddress: string, address:string, note: string,type:string): Promise {
+ try {
+ const axiosInstance = this.createAxiosInstance(ipAddress);
+ return await axiosInstance.post(`/subnet/${type}/${address}/note`, {
+ note: note
+ });
+ } catch (error: any) {
+ return error.response;
+ }
+ }
+ static async editNote(ipAddress: string, address:string, note: string,type:string): Promise {
+ try {
+ const axiosInstance = this.createAxiosInstance(ipAddress);
+ return await axiosInstance.put(`/subnet/${type}/${address}/note`, {
+ note: note
+ });
+ } catch (error: any) {
+ return error.response;
+ }
+ }
+ static async deleteNote(ipAddress: string, address:string, type:string): Promise {
+ try {
+ const axiosInstance = this.createAxiosInstance(ipAddress);
+ return await axiosInstance.delete(`/subnet/${type}/${address}/note`);
+ } catch (error: any) {
+ return error.response;
+ }
+ }
+ static async statusNetwork(ipAddress: string): Promise {
+ try {
+ const axiosInstance = this.createAxiosInstance(ipAddress);
+ return await axiosInstance.get(`/subnet/wifi/status`);
+ } catch (error: any) {
+ return error.response;
+ }
+ }
+ static async connectNetwork(ipAddress: string, password:string, address: string): Promise {
+ try {
+ const axiosInstance = this.createAxiosInstance(ipAddress);
+ return await axiosInstance.post(`/subnet/wifi/connect`, {
+ ssid: address,
+ password: password,
+ });
+ } catch (error: any) {
+ return error.response;
+ }
+ }
+ static async disconnectNetwork(ipAddress: string, ssid: string): Promise {
+ try {
+ const axiosInstance = this.createAxiosInstance(ipAddress);
+ return await axiosInstance.post(`/subnet/wifi/disconnect`, {
+ ssid: ssid,
+ });
+ } catch (error: any) {
+ return error.response;
+ }
+ }
}
\ No newline at end of file
diff --git a/FRONT/BUILD/resources/app/src/components/UI/Dropdown/DeviceCard.tsx b/FRONT/BUILD/resources/app/src/components/UI/Dropdown/DeviceCard.tsx
new file mode 100644
index 0000000..cfed6c4
--- /dev/null
+++ b/FRONT/BUILD/resources/app/src/components/UI/Dropdown/DeviceCard.tsx
@@ -0,0 +1,137 @@
+import React, {useState} from 'react';
+import styled, { css } from 'styled-components';
+import { DeviceUser } from "../../../store/useDeviceStore";
+import {buttonNames} from "../../../const/buttons/names";
+import {ModalMap} from "../../modalwindows/windowsType";
+import {IconComponent} from "../../../const/buttons/icons";
+import {useModalContext} from "../../../store";
+
+interface DeviceCardProps {
+ device: DeviceUser;
+ status: 'online' | 'offline';
+}
+
+const CardWrapper = styled.div<{ status: 'online' | 'offline' }>`
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ transition: all 0.3s ease;
+ cursor: pointer;
+ margin-bottom: 16px;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
+ }
+`;
+
+const CardHeader = styled.div<{ status: 'online' | 'offline' }>`
+ background: ${({ status, theme }) =>
+ status === 'online' ? theme.successColor : theme.offlineColor};
+ color: white;
+ padding: 16px;
+ font-size: 18px;
+ font-weight: 500;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+
+const CardContent = styled.div<{ isExpanded: boolean }>`
+ max-height: 0;
+ opacity: 0;
+ padding: 0 16px;
+ transition: all 0.3s ease;
+ color: #333;
+ font-size: 14px;
+ display: grid;
+ gap: 12px;
+
+ ${({ isExpanded }) => isExpanded && css`
+ max-height: 500px;
+ opacity: 1;
+ padding: 16px;
+ `}
+`;
+
+const InfoRow = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+`;
+
+const Label = styled.span`
+ font-weight: 500;
+ color: #666;
+`;
+
+const Value = styled.span`
+ color: #333;
+`;
+
+
+const DeviceCard: React.FC = ({ device, status }) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const toggleExpanded = () => setIsExpanded(!isExpanded);
+ const { openModal } = useModalContext();
+
+ const openModalNote = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ openModal(ModalMap['6'], {
+ address: device.mac,
+ currentNote: device.note || ''
+ });
+ };
+
+
+ return (
+
+
+
+ MAC: {device.mac}
+
+
+
+
+
+
+
+
+ Имя:
+ {device.hostname || 'Неизвестно'}
+
+
+
+ Производитель:
+ {device.vendor || 'Неизвестно'}
+
+
+
+ IP адрес:
+ {device.ip}
+
+
+ {device.open_ports?.length > 0 && (
+
+ Открытые порты:
+ {device.open_ports.join(', ')}
+
+ )}
+
+
+ Последняя активность:
+ {device.last_seen}
+
+
+
+ Заметка:
+ {device.note || '-'}
+
+
+
+ );
+};
+
+export default DeviceCard;
\ No newline at end of file
diff --git a/FRONT/BUILD/resources/app/src/components/UI/Dropdown/DropdownType.tsx b/FRONT/BUILD/resources/app/src/components/UI/Dropdown/DropdownType.tsx
new file mode 100644
index 0000000..d1e90c5
--- /dev/null
+++ b/FRONT/BUILD/resources/app/src/components/UI/Dropdown/DropdownType.tsx
@@ -0,0 +1,130 @@
+import React, {useEffect, useRef, useState} from 'react';
+import styled from "styled-components";
+import {buttonNames} from "../../../const/buttons/names";
+import {IconComponent} from "../../../const/buttons/icons";
+
+type Option = {
+ value: number,
+ label: string,
+}
+
+interface DropdownTypeProps {
+ options: Option[];
+ label?: string;
+ onChange?: (selectedItem: Option) => void;
+ value: Option | null;
+}
+
+const DropdownWrapper = styled.div`
+ position: absolute;
+ top: 18px;
+ right:20px;
+ display: inline-block;
+ background-color: ${({theme}) => theme.background};
+ padding: 12px 16px;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ z-index: 10;
+`
+const Label = styled.p`
+ color: ${({theme}) => theme.textColor};
+ font-size: 16px;
+`
+
+const Dropdown = styled.div`
+ background-color: ${({theme}) => theme.primary};
+ border-radius: 15px;
+ overflow: hidden;
+ transition: all .3s ease;
+ cursor: pointer;
+ z-index: 10;
+ margin-top: 6px;
+`
+
+const DropdownHeader = styled.div`
+ display: flex;
+ justify-content: space-between;
+ gap: 14px;
+ padding: 12px 24px;
+ font-size: 16px;
+ align-items: center;
+ height: 100%;
+ min-width: 200px;
+`
+
+const DropdownContent = styled.div<{$isOpen: boolean}>`
+ max-height: ${({$isOpen}) => $isOpen ? '500px' : '0'};
+ opacity: ${({$isOpen}) => $isOpen ? 1 : 0};
+ transition: all .3s ease;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+`
+
+const DropdownRow = styled.button`
+ padding: 12px 24px;
+ background-color: transparent;
+ text-align: start;
+ font-size: 16px;
+ border: none;
+ width: 100%;
+ color: ${({theme}) => theme.textColor};
+ transition: all .3s ease;
+ cursor: pointer;
+
+ &:hover {
+ background-color: ${({theme}) => theme.background};
+ }
+`
+
+const DropdownArrow = styled.div<{ $isOpen: boolean }>`
+ transition: transform 0.3s ease;
+ transform: rotate(${({ $isOpen }) => ($isOpen ? "90deg" : "0deg")});
+`;
+
+const DropdownType = ({options, value, onChange, label}: DropdownTypeProps) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ function toggleOpen() {
+ setIsOpen((prev) => !prev);
+ }
+
+ function handleSelect(option: Option) {
+ onChange?.(option);
+ setIsOpen(false);
+ }
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ return (
+
+ {label}
+
+
+ {value?.label || 'Select an option'}
+
+
+
+
+
+ {options.map((option, i) => (
+ handleSelect(option)}>
+ {option.label}
+
+ ))}
+
+
+
+ );
+};
+
+export default DropdownType;
\ No newline at end of file
diff --git a/FRONT/BUILD/resources/app/src/components/UI/buttons/mainButton.tsx b/FRONT/BUILD/resources/app/src/components/UI/buttons/mainButton.tsx
index 0afe0a2..55ca7e3 100644
--- a/FRONT/BUILD/resources/app/src/components/UI/buttons/mainButton.tsx
+++ b/FRONT/BUILD/resources/app/src/components/UI/buttons/mainButton.tsx
@@ -9,15 +9,15 @@ const Button = styled.button.withConfig({
border: none;
padding: ${(props) =>
props.size === "small"
- ? "4px 8px"
+ ? "10px 28px"
: props.size === "big"
? "10px 20px"
- : "6px 10px"}; // По умолчанию
+ : "12px 20px"}; // По умолчанию
border-radius: 8px;
color: white;
font-size: ${(props) =>
props.size === "small"
- ? "12px"
+ ? "14px"
: props.size === "big"
? "16px"
: "14px"}; // По умолчанию
@@ -30,7 +30,7 @@ const Button = styled.button.withConfig({
? 'row-reverse'
: 'row'};
gap: 10px;
-
+ transition: all .3s ease;
&:hover{
background-color: ${(props) => props.$hoverсolor};
}
diff --git a/FRONT/BUILD/resources/app/src/components/UI/inputs/input.tsx b/FRONT/BUILD/resources/app/src/components/UI/inputs/input.tsx
index 0fac915..dd2b9c9 100644
--- a/FRONT/BUILD/resources/app/src/components/UI/inputs/input.tsx
+++ b/FRONT/BUILD/resources/app/src/components/UI/inputs/input.tsx
@@ -5,7 +5,7 @@ type Props = {
placeholderText: string;
id: string;
value?: string;
- onChange?: (identifier: string, value: string) => void;
+ onChange?: (value: string) => void;
onFileChange?: (event: React.ChangeEvent) => void;
type?: string;
multiple?: boolean;
@@ -33,7 +33,7 @@ const StyledInput = styled.input`
border: 1px solid #adadad;
}
width: 100%;
- font-size: 14px;
+ font-size: 18px;
color: ${props=>props.theme.textColor}
`;
@@ -41,7 +41,7 @@ const Input = React.forwardRef(({ placeholderText, id,
const handleInputChange = (e: React.ChangeEvent) => {
const { value } = e.target;
if (onChange) {
- onChange(id, value);
+ onChange(value);
}
};
diff --git a/FRONT/BUILD/resources/app/src/components/UI/signalSvg.tsx b/FRONT/BUILD/resources/app/src/components/UI/signalSvg.tsx
index a8c5c32..9590433 100644
--- a/FRONT/BUILD/resources/app/src/components/UI/signalSvg.tsx
+++ b/FRONT/BUILD/resources/app/src/components/UI/signalSvg.tsx
@@ -2,19 +2,21 @@ import React, { useEffect, useRef } from 'react';
interface SignalSVGProps {
signal: number; // Принимаем signal как пропс
+ color: string;
+ size: string;
}
-const SignalSVG: React.FC = ({ signal }) => {
+const SignalSVG: React.FC = ({ signal, color, size }) => {
const svgRef = useRef(null);
useEffect(() => {
// Устанавливаем цвета в зависимости от signal
const colors = {
- '--st1-color': signal >= 1 ? 'white' : 'black',
- '--st2-color': signal >= 2 ? 'white' : 'black',
- '--st3-color': signal >= 3 ? 'white' : 'black',
- '--st4-color': signal >= 4 ? 'white' : 'black',
- '--st5-color': signal === 5 ? 'white' : 'black',
+ '--st1-color': signal >= 1 ? color : 'black',
+ '--st2-color': signal >= 2 ? color : 'black',
+ '--st3-color': signal >= 3 ? color : 'black',
+ '--st4-color': signal >= 4 ? color : 'black',
+ '--st5-color': signal === 5 ? color : 'black',
};
// Применяем стили к SVG
@@ -35,8 +37,8 @@ const SignalSVG: React.FC = ({ signal }) => {
y="0px"
viewBox="0 0 296 235"
xmlSpace="preserve"
- width="20px" // Фиксированная ширина
- height="20px"
+ width={size} // Фиксированная ширина
+ height={size}
>
+
+
+
+
+
+
+
+ Contents
+
+
+
+
+
+
+ Expand
+
+
+
+
+
+ Light mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dark mode
+
+
+
+
+
+
+ Auto light/dark, in light mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auto light/dark, in dark mode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Skip to content
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to top
+
+
+
+ Source code for api.v1.auth
+from fastapi import APIRouter , Depends , HTTPException , Request
+from fastapi.security import OAuth2PasswordBearer
+from services.auth_service import AuthService
+from models.auth import UserCreate , UserLogin , UserResponse , Token
+from storage.json_storage import JsonStorage
+from utils.i18n import get_translations , _
+from babel.support import Translations
+import logging
+import os
+
+logging . basicConfig ( level = logging . INFO )
+logger = logging . getLogger ( __name__ )
+
+router = APIRouter ()
+auth_service = AuthService ()
+oauth2_scheme = OAuth2PasswordBearer ( tokenUrl = "/auth/login" )
+
+
+
[docs]
+
@router . post ( "/register" , response_model = UserResponse )
+
async def register ( user : UserCreate , translations : Translations = Depends ( get_translations ())):
+
"""Регистрирует нового пользователя.
+
+
Создаёт нового пользователя на основе предоставленных данных и возвращает
+
информацию о зарегистрированном пользователе.
+
+
Args:
+
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
+
translations: Объект переводов для текущего языка.
+
+
Returns:
+
UserResponse: Объект с данными зарегистрированного пользователя (id, username, is_admin).
+
+
Raises:
+
HTTPException: Если пользователь уже существует (status_code=400) или произошла
+
внутренняя ошибка (status_code=500).
+
"""
+
try :
+
db_user = auth_service . register ( user )
+
logger . info ( f "Пользователь { user . username } успешно зарегистрирован" )
+
return db_user
+
except HTTPException as e :
+
raise e
+
except Exception as e :
+
logger . error ( f "Error in register: { type ( e ) . __name__ } - { str ( e ) } " )
+
raise HTTPException ( status_code = 500 , detail = translations . gettext ( "Не удалось зарегистрировать пользователя" ))
+
+
+
+
[docs]
+
@router . post ( "/login" , response_model = Token )
+
async def login ( user : UserLogin , request : Request , translations : Translations = Depends ( get_translations ())):
+
"""Авторизует пользователя и создаёт сессию.
+
+
Проверяет учетные данные пользователя и возвращает токен и session_id для
+
последующей авторизации.
+
+
Args:
+
user (UserLogin): Объект с данными для входа (username, password).
+
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
+
translations: Объект переводов для текущего языка.
+
+
Returns:
+
Token: Объект с access_token и session_id для авторизации.
+
+
Raises:
+
HTTPException: Если учетные данные неверны (status_code=401) или произошла
+
внутренняя ошибка (status_code=500).
+
"""
+
try :
+
client_id = request . client . host
+
logger . debug ( f "Попытка входа в систему клиента { client_id } " )
+
result = auth_service . login ( user , client_id )
+
logger . info ( f "Успешная авторизация для клиента { client_id } , Идентификатор сессии: { result [ 'session_id' ] } " )
+
return result
+
except HTTPException as e :
+
raise e
+
except Exception as e :
+
logger . error ( f "Ошибка при авторизации: { type ( e ) . __name__ } - { str ( e ) } " )
+
raise HTTPException ( status_code = 500 , detail = translations . gettext ( "Не удалось авторизовать пользователя" ))
+
+
+
+
[docs]
+
@router . get ( "/me" )
+
async def get_current_user ( request : Request , translations : Translations = Depends ( get_translations ())):
+
"""Возвращает данные текущего пользователя и токен.
+
+
Проверяет активную сессию по client_id и возвращает информацию о пользователе
+
и токен для дальнейшего использования.
+
+
Args:
+
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
+
translations: Объект переводов для текущего языка.
+
+
Returns:
+
dict: Словарь с ключами "user" (UserResponse), "access_token" (str) и
+
"token_type" (str, всегда "bearer").
+
+
Raises:
+
HTTPException: Если сессия не найдена или истекла (status_code=401) или
+
произошла внутренняя ошибка (status_code=500).
+
"""
+
try :
+
client_id = request . client . host
+
logger . debug ( f "Запрос /me для клиента { client_id } " )
+
result = auth_service . get_user_by_session ( client_id )
+
return {
+
"user" : result [ "user" ],
+
"access_token" : result [ "access_token" ],
+
"token_type" : translations . gettext ( "bearer" ),
+
}
+
except HTTPException as e :
+
raise e
+
except Exception as e :
+
logger . error ( f "Ошибка при get_current_user: { type ( e ) . __name__ } - { str ( e ) } " )
+
raise HTTPException ( status_code = 500 , detail = translations . gettext ( "Не удалось получить данные пользователя" ))
+
+
+
+
[docs]
+
@router . get ( "/logout" )
+
async def logout ( request : Request , translations : Translations = Depends ( get_translations ())):
+
"""Выход из системы, удаление сессии.
+
+
Удаляет активную сессию пользователя по client_id.
+
+
Args:
+
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
+
translations: Объект переводов для текущего языка.
+
+
Returns:
+
dict: Пустой словарь или сообщение об успешном выходе.
+
+
Raises:
+
HTTPException: Если сессия не найдена (status_code=401) или произошла
+
внутренняя ошибка (status_code=500).
+
"""
+
try :
+
client_id = request . client . host
+
logger . debug ( f "Запрос на выход из системы для клиента { client_id } " )
+
return auth_service . logout ( client_id )
+
except HTTPException as e :
+
raise e
+
except Exception as e :
+
logger . error ( f "Не удалось выйти из системы: { type ( e ) . __name__ } - { str ( e ) } " )
+
raise HTTPException ( status_code = 500 , detail = translations . gettext ( "Не удалось выйти из системы" ))
+
+
+
+
[docs]
+
@router . get ( "/check_json" )
+
async def check_users_file ( translations : Translations = Depends ( get_translations ())):
+
"""Проверяет, существует ли файл users.json и содержит ли он пользователей.
+
+
Возвращает информацию о состоянии файла и наличии пользователей.
+
+
Returns:
+
dict: Словарь с ключами:
+
- "exists" (bool): Существует ли файл.
+
- "has_users" (bool): Содержит ли файл пользователей.
+
- "message" (str): Сообщение с описанием результата.
+
+
Raises:
+
HTTPException: Если произошла ошибка при проверке файла (status_code=500).
+
"""
+
try :
+
file_path = "/etc/gonets/users.json"
+
storage = JsonStorage ( file_path = file_path )
+
+
if not os . path . exists ( file_path ):
+
logger . info ( f "Файл { file_path } не существует" )
+
return {
+
"exists" : False ,
+
"has_users" : False ,
+
"message" : translations . gettext ( "Файл users.json не существует" ),
+
}
+
+
data = storage . _read_data ()
+
has_users = len ( data . get ( "users" , [])) > 0
+
if has_users :
+
logger . info ( f "Файл { file_path } существует и содержит пользователей" )
+
return {
+
"exists" : True ,
+
"has_users" : True ,
+
"message" : translations . gettext ( "Файл users.json существует и содержит пользователей" ),
+
}
+
else :
+
logger . info ( f "Файл { file_path } существует, но не содержит пользователей" )
+
return {
+
"exists" : True ,
+
"has_users" : False ,
+
"message" : translations . gettext ( "Файл users.json существует, но не содержит пользователей" ),
+
}
+
except Exception as e :
+
logger . error ( f "Ошибка при проверке файла: { type ( e ) . __name__ } - { str ( e ) } " )
+
raise HTTPException ( status_code = 500 , detail = translations . gettext ( "Ошибка при проверке файла" ))
+
+
+
+
+
+
+
+
+
+
+
+
+
+