diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea15021 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.venv +.idea +__pycache__ +.gigaide +.vscode +.wheels +.vendor +.python3-deb +SRV/storage/devices/devices.json +SRV/storage/wifi/wifi.json \ No newline at end of file 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/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..0345346 --- /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.ip} + + + + + {device.vendor || 'Неизвестно'} + + + + + {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 ( + + + + +

{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} >