Compare commits

...

No commits in common. "main" and "v0.0.2" have entirely different histories.
main ... v0.0.2

721 changed files with 209030 additions and 65200 deletions

1
.pc/.quilt_patches Normal file
View file

@ -0,0 +1 @@
debian/patches

1
.pc/.quilt_series Normal file
View file

@ -0,0 +1 @@
series

1
.pc/.version Normal file
View file

@ -0,0 +1 @@
2

View file

@ -0,0 +1,75 @@
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} не найдено")

View file

@ -0,0 +1,112 @@
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} не найдено")

2
.pc/applied-patches Normal file
View file

@ -0,0 +1,2 @@
1
2

View file

@ -1,137 +0,0 @@
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 "Ошибка загрузки файла"

View file

@ -1,13 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GoNets</title>
<link rel="icon" type="image/x-icon" href="/assets/icon-4fEsvohP.png" />
<script type="module" crossorigin src="/assets/index-QSWEVNb0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DWFkiel1.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View file

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';
import { NetworkService } from './src/api/request.ts';
import { useIpStore } from './src/store'; import { useIpStore } from './src/store';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useAdminStore } from './src/store/adminStore.ts';
import {useCheckAdmin} from './src/utils/checkAdmin.ts' import {useCheckAdmin} from './src/utils/checkAdmin.ts'
import { import {

View file

@ -21,7 +21,7 @@ function createWindow() {
win.show(); win.show();
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
win.loadURL('https://localhost:8001'); win.loadURL('http://localhost:3000');
win.webContents.openDevTools(); win.webContents.openDevTools();
} else { } else {
win.loadFile(path.join(__dirname, 'dist/index.html')); win.loadFile(path.join(__dirname, 'dist/index.html'));

View file

@ -11,7 +11,6 @@
}, },
"dependencies": { "dependencies": {
"@iconify/icons-ic": "^1.2.13", "@iconify/icons-ic": "^1.2.13",
"@iconify/icons-ph": "^1.2.5",
"@iconify/react": "^5.2.0", "@iconify/react": "^5.2.0",
"@reduxjs/toolkit": "^2.6.0", "@reduxjs/toolkit": "^2.6.0",
"@xyflow/react": "^12.3.5", "@xyflow/react": "^12.3.5",

View file

@ -82,79 +82,4 @@ export class NetworkService {
return error.response; return error.response;
} }
} }
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/devices');
} catch (error: any) {
return error.response;
}
}
static async scanWifi(ipAddress: string): Promise<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/wifi/disconnect`, {
ssid: ssid,
});
} catch (error: any) {
return error.response;
}
}
} }

View file

@ -1,137 +0,0 @@
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<DeviceCardProps> = ({ 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 (
<CardWrapper status={status} onClick={toggleExpanded}>
<CardHeader status={status}>
<p>
MAC: {device.mac}
</p>
<span onClick={openModalNote}>
<IconComponent name={buttonNames[16]} width="28" height="28" setColor={'white'} />
</span>
</CardHeader>
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>Производитель:</Label>
<Value>{device.vendor || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>IP адрес:</Label>
<Value>{device.ip}</Value>
</InfoRow>
{device.open_ports?.length > 0 && (
<InfoRow>
<Label>Открытые порты:</Label>
<Value>{device.open_ports.join(', ')}</Value>
</InfoRow>
)}
<InfoRow>
<Label>Последняя активность:</Label>
<Value>{device.last_seen}</Value>
</InfoRow>
<InfoRow>
<Label>Заметка:</Label>
<Value>{device.note || '-'}</Value>
</InfoRow>
</CardContent>
</CardWrapper>
);
};
export default DeviceCard;

View file

@ -1,130 +0,0 @@
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<HTMLDivElement>(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 (
<DropdownWrapper ref={dropdownRef}>
<Label>{label}</Label>
<Dropdown>
<DropdownHeader onClick={toggleOpen}>
<p>{value?.label || 'Select an option'}</p>
<DropdownArrow $isOpen={isOpen}>
<IconComponent name={buttonNames[6]} />
</DropdownArrow>
</DropdownHeader>
<DropdownContent $isOpen={isOpen}>
{options.map((option, i) => (
<DropdownRow key={i} onClick={() => handleSelect(option)}>
{option.label}
</DropdownRow>
))}
</DropdownContent>
</Dropdown>
</DropdownWrapper>
);
};
export default DropdownType;

View file

@ -9,15 +9,15 @@ const Button = styled.button.withConfig({
border: none; border: none;
padding: ${(props) => padding: ${(props) =>
props.size === "small" props.size === "small"
? "10px 28px" ? "4px 8px"
: props.size === "big" : props.size === "big"
? "10px 20px" ? "10px 20px"
: "12px 20px"}; // По умолчанию : "6px 10px"}; // По умолчанию
border-radius: 8px; border-radius: 8px;
color: white; color: white;
font-size: ${(props) => font-size: ${(props) =>
props.size === "small" props.size === "small"
? "14px" ? "12px"
: props.size === "big" : props.size === "big"
? "16px" ? "16px"
: "14px"}; // По умолчанию : "14px"}; // По умолчанию
@ -30,7 +30,7 @@ const Button = styled.button.withConfig({
? 'row-reverse' ? 'row-reverse'
: 'row'}; : 'row'};
gap: 10px; gap: 10px;
transition: all .3s ease;
&:hover{ &:hover{
background-color: ${(props) => props.$hoverсolor}; background-color: ${(props) => props.$hoverсolor};
} }

View file

@ -5,7 +5,7 @@ type Props = {
placeholderText: string; placeholderText: string;
id: string; id: string;
value?: string; value?: string;
onChange?: (value: string) => void; onChange?: (identifier: string, value: string) => void;
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
type?: string; type?: string;
multiple?: boolean; multiple?: boolean;
@ -33,7 +33,7 @@ const StyledInput = styled.input`
border: 1px solid #adadad; border: 1px solid #adadad;
} }
width: 100%; width: 100%;
font-size: 18px; font-size: 14px;
color: ${props=>props.theme.textColor} color: ${props=>props.theme.textColor}
`; `;
@ -41,7 +41,7 @@ const Input = React.forwardRef<HTMLInputElement, Props>(({ placeholderText, id,
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target; const { value } = e.target;
if (onChange) { if (onChange) {
onChange(value); onChange(id, value);
} }
}; };

View file

@ -2,21 +2,19 @@ import React, { useEffect, useRef } from 'react';
interface SignalSVGProps { interface SignalSVGProps {
signal: number; // Принимаем signal как пропс signal: number; // Принимаем signal как пропс
color: string;
size: string;
} }
const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => { const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
const svgRef = useRef<SVGSVGElement | null>(null); const svgRef = useRef<SVGSVGElement | null>(null);
useEffect(() => { useEffect(() => {
// Устанавливаем цвета в зависимости от signal // Устанавливаем цвета в зависимости от signal
const colors = { const colors = {
'--st1-color': signal >= 1 ? color : 'black', '--st1-color': signal >= 1 ? 'white' : 'black',
'--st2-color': signal >= 2 ? color : 'black', '--st2-color': signal >= 2 ? 'white' : 'black',
'--st3-color': signal >= 3 ? color : 'black', '--st3-color': signal >= 3 ? 'white' : 'black',
'--st4-color': signal >= 4 ? color : 'black', '--st4-color': signal >= 4 ? 'white' : 'black',
'--st5-color': signal === 5 ? color : 'black', '--st5-color': signal === 5 ? 'white' : 'black',
}; };
// Применяем стили к SVG // Применяем стили к SVG
@ -37,8 +35,8 @@ const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
y="0px" y="0px"
viewBox="0 0 296 235" viewBox="0 0 296 235"
xmlSpace="preserve" xmlSpace="preserve"
width={size} // Фиксированная ширина width="20px" // Фиксированная ширина
height={size} height="20px"
> >
<style type="text/css"> <style type="text/css">
{` {`

View file

@ -1,109 +0,0 @@
import React from 'react';
import { DeviceUser } from '../../../store/useDeviceStore';
interface TableScannerProps {
devices: DeviceUser[];
}
import styled from 'styled-components';
import {IconComponent} from "../../../const/buttons/icons";
import {buttonNames} from "../../../const/buttons/names";
import {ModalMap} from "../../modalwindows/windowsType";
import {useModalContext} from "../../../store";
const TableContainer = styled.div`
width: 100%;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
background: ${({ theme }) => theme.primary};
margin: 24px 0;
`;
const StyledTable = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
`;
const TableHeaderCell = styled.th<{ sortable?: boolean }>`
padding: 16px 24px;
text-align: left;
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.textSecondary};
background: ${({ theme }) => theme.primary};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const TableRow = styled.tr<{ clickable?: boolean }>`
transition: all 0.2s ease;
&:hover {
background-color: ${({ theme }) => theme.background};
}
`;
const TableCell = styled.td`
padding: 16px 24px;
font-size: 14px;
color: ${({ theme }) => theme.text};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const StatusBadge = styled.span<{ status:string }>`
width: 10px;
height: 30px;
background-color: ${({theme, status}) => (status==='online' ? theme.successColor : theme.offlineColor)};
display: inline-block;
border-radius:4px;
`
const TableScanner = ({devices}:TableScannerProps) => {
const { openModal } = useModalContext();
const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
return (
<TableContainer>
<StyledTable>
<thead>
<tr>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell>Имя</TableHeaderCell>
<TableHeaderCell>Производитель</TableHeaderCell>
<TableHeaderCell>IP адрес</TableHeaderCell>
<TableHeaderCell>Открытые порты</TableHeaderCell>
<TableHeaderCell>Последняя активность</TableHeaderCell>
<TableHeaderCell>Заметка</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</tr>
</thead>
<tbody>
{devices.map((item, i) => (
<TableRow key={i}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
<TableCell>{item.hostname || 'Неизвестен'}</TableCell>
<TableCell>{item.vendor}</TableCell>
<TableCell>{item.ip}</TableCell>
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
<TableCell>{item.last_seen}</TableCell>
<TableCell>{item.note || '-'}</TableCell>
<TableCell>
<span onClick={(e)=>openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" />
</span>
</TableCell>
</TableRow>
))}
</tbody>
</StyledTable>
</TableContainer>
);
};
export default TableScanner;

View file

@ -184,7 +184,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipAdress} placeholderText={data.ipData.ipAdress}
id="newIp" id="newIp"
value={ipData.newIp} value={ipData.newIp}
onChange={(value) => handleChange('newIp', value)} onChange={(id, value) => handleChange(id, value)}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}} style={{fontSize: '12px', padding: '4px 10px'}}
@ -203,7 +203,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipGate} placeholderText={data.ipData.ipGate}
id="newGate" id="newGate"
value={ipData.newGate} value={ipData.newGate}
onChange={(value) => handleChange('newGate', value)} onChange={(id, value) => handleChange(id, value)}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}} style={{fontSize: '12px', padding: '4px 10px'}}
@ -221,7 +221,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipDns.join(', ')} placeholderText={data.ipData.ipDns.join(', ')}
id="newDns" id="newDns"
value={ipData.newDns.join(', ')} value={ipData.newDns.join(', ')}
onChange={( value) => handleChange('newDns', value)} onChange={(id, value) => handleChange(id, value)}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}} style={{fontSize: '12px', padding: '4px 10px'}}

View file

@ -73,7 +73,7 @@ export const NetWorkNode = ({ data }: CustomNodeProps) => {
{data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? ( {data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? (
<Status> <Status>
<SignalSVG signal={signal} color={'white'} size={'20px'} /> <SignalSVG signal={signal} />
</Status> </Status>
) : ( ) : (
<div></div> <div></div>

View file

@ -18,7 +18,7 @@ export const Network = styled.div<Props>`
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
visibility: visible; visibility: visible;
overflow: hidden;
p{ p{
margin: 0; margin: 0;
} }
@ -29,6 +29,8 @@ export const TitleContainer = styled.div<Props>`
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: ${(props) => props.$backgroundcolor}; background: ${(props) => props.$backgroundcolor};
border-top-left-radius: 8px;
border-top-right-radius: 8px;
color: white; color: white;
padding: 15px; padding: 15px;
height: ${(props) => props.$height ? props.$height : 'unset'}; height: ${(props) => props.$height ? props.$height : 'unset'};

View file

@ -1,84 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useIpStore} from "../../../store";
interface Props {
address: string;
onClose: () => void;
}
export const ConnectNetwork = ({address, onClose}: Props) => {
const [password, setPassword] = useState('');
const theme = useTheme();
const { ipAddress } = useIpStore();
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSave = async () => {
try {
setIsLoading(true);
setError('')
const res = await NetworkService.connectNetwork(ipAddress, password, address);
if(res.status === 200) {
onClose()
}else{
setError(res.data.detail)
}
} catch (error) {
console.error('Error connection:', error);
setError('Ошибка подключения к сети')
}finally {
setIsLoading(false);
}
};
return (
<ModalContainer style={{ height: '230px' }}>
<ModalTitle>
<h3>Подключение к сети</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="password"
placeholderText='Введите пароль...'
value={password}
id='deviceNote'
onChange={(value) => setPassword(value)}
/>
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
{error}
</p>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
color={theme.successColor}
hoverColor={theme.successColor}
title='Подключиться'
onClick={handleSave}
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>}
</ModalContainer>
);
};

View file

@ -1,82 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {useIpStore} from "../../../store";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { devices, setDevices } = useNetworkStore();
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedDevices = devices.map(device =>
device.mac === address
? { ...device, note: note.trim() }
: device
);
setDevices(updatedDevices);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '250px' }}>
<ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText='Введите заметку...'
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title='Сохранить'
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -1,82 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useIpStore} from "../../../store";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedNetworks = wifiNetworks.map(network =>
network.ssid === address
? { ...network, note: note.trim() }
: { ...network }
);
setWifiNetworks(updatedNetworks);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '220px' }}>
<ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText='Введите заметку...'
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title='Сохранить'
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -39,7 +39,7 @@ export const AddDevice = ({ onSave, onClose }: Props) => {
placeholderText='Введите адрес узла...' placeholderText='Введите адрес узла...'
id="newIp" id="newIp"
value={ipAddress} value={ipAddress}
onChange={(value) => setIpAddress(value)} onChange={(id, value) => setIpAddress(value)}
/> />
{error && ( {error && (
<p style={{ color: 'red', marginTop: '5px' }}> <p style={{ color: 'red', marginTop: '5px' }}>

View file

@ -1,54 +0,0 @@
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
import { useModalContext } from "../../../store"
import { useIpStore } from "../../../store"
import { MainButton } from "../../UI/buttons/mainButton"
import {useState} from "react";
import {NetworkService} from "../../../api/request";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
type Props={
address: string,
onClose: () => void
}
export const DisconnectConfirm = ({address, onClose}: Props)=>{
const { ipAddress } = useIpStore();
const theme = useTheme()
const [isLoading, setIsLoading] = useState(false);
const handleDisconnect = async () => {
try {
setIsLoading(true);
const res = await NetworkService.disconnectNetwork(ipAddress, address);
if(res.status === 200) {
onClose()
}
} catch (error) {
console.error('Error connection:', error);
}finally {
setIsLoading(false);
}
};
return(
<ModalContainer>
<ModalTitle>
<h3>Отключение от Wi-Fi сети</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>Подтверждение отключения</h3>
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton title="Отменить" onClick={onClose} size="small"/>
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Отключение от сети'/>}
</ModalContainer>
)
}

View file

@ -110,16 +110,14 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
try { try {
const result = await NetworkService.saveIp(ipAddress, data); const result = await NetworkService.saveIp(ipAddress, data);
if (result.status === 200) { if (result.status === 200) {
if(ipAddress!=='localhost'){ setIpAddress(ipData.newIp)
setIpAddress(ipData.newIp)
const updatedDevices = devices.map(device => const updatedDevices = devices.map(device =>
device.address === ipData.currentIp device.address === ipData.currentIp
? { ...device, address: ipData.newIp } ? { ...device, address: ipData.newIp }
: device : device
); );
useDevicesStore.setState({ devices: updatedDevices }); useDevicesStore.setState({ devices: updatedDevices });
}
onClose() onClose()
} }

View file

@ -87,7 +87,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
placeholderText='Введите пароль администратора...' placeholderText='Введите пароль администратора...'
id="password" id="password"
value={user.password} value={user.password}
onChange={(value) => handleChange('password', value)} onChange={(id, value) => handleChange(id, value)}
/> />
{error && ( {error && (

View file

@ -3,10 +3,6 @@ import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalCon
import { ModalMap } from "./windowsType"; import { ModalMap } from "./windowsType";
import Loader from "../UI/loader"; import Loader from "../UI/loader";
import { ModalWindow } from "./style"; import { ModalWindow } from "./style";
import {NoteDevice} from "./modalContainers/NoteDevice";
import { NoteWifi } from "./modalContainers/NoteWifi";
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
type Props = { type Props = {
isOpen: boolean, isOpen: boolean,
@ -39,34 +35,7 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
message={data.message} message={data.message}
/> />
} }
{type === ModalMap['6'] && (
<NoteDevice
type={'devices'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['7'] && (
<NoteWifi
type={'wifi'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['8'] && (
<ConnectNetwork
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['9'] && (
<DisconnectConfirm
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['4'] && ( {type === ModalMap['4'] && (
<AddDevice onSave={data?.onSave} onClose={closeLoader} /> <AddDevice onSave={data?.onSave} onClose={closeLoader} />
)} )}

View file

@ -24,7 +24,7 @@ export const ModalContainer = styled.div<{height?: string}>`
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
gap: 15px; gap: 15px;
height: ${(props) => props.height ? props.height : '230px'}; height: ${(props) => props.height ? props.height : '200px'};
width: 520px; width: 520px;
` `
@ -66,5 +66,5 @@ export const ButtonContainer = styled.div`
justify-content: flex-end; justify-content: flex-end;
display: flex; display: flex;
gap: 10px; gap: 10px;
padding: 10px 20px 20px; padding: 20px;
` `

View file

@ -5,9 +5,5 @@ export const ModalMap: ModalType = {
'2': 'loader', '2': 'loader',
'3': 'connectionError', '3': 'connectionError',
'4': 'addDevice', '4': 'addDevice',
'5': 'sudo', '5': 'sudo'
'6': 'noteDevice',
'7': 'noteWifi',
'8': 'connectNetwork',
'9': 'disconnectConfirm',
} }

View file

@ -101,6 +101,7 @@ export const CreateUser = () => {
if (result.status === 200) { window.location.reload() } if (result.status === 200) { window.location.reload() }
} catch (err) { } catch (err) {
console.error('Error:', err); console.error('Error:', err);
} finally {
} }
} }
@ -145,7 +146,7 @@ export const CreateUser = () => {
placeholderText='Введите пароль...' placeholderText='Введите пароль...'
id="password" id="password"
value={user.password} value={user.password}
onChange={(value) => handleChange('password', value)} onChange={(id, value) => handleChange(id, value)}
/> />
<Input <Input
@ -153,7 +154,7 @@ export const CreateUser = () => {
placeholderText='Повторите пароль...' placeholderText='Повторите пароль...'
id="repeatPassword" id="repeatPassword"
value={repeatPassword} value={repeatPassword}
onChange={(value) => handleChange('repeatPassword', value)} onChange={(id, value) => handleChange(id, value)}
style={{ marginTop: '10px' }} style={{ marginTop: '10px' }}
/> />
{!passwordsMatch && ( {!passwordsMatch && (

View file

@ -1,82 +1,82 @@
import styled from "styled-components" import styled from "styled-components"
import { MainButton } from "../../../UI/buttons/mainButton" import { MainButton } from "../../../UI/buttons/mainButton"
import { useWorkSpaceStore } from "../../../../store" import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names" import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names" import { buttonNames } from "../../../../const/buttons/names"
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: ${(props => props.theme.background)}; background-color: ${(props => props.theme.background)};
background-image: url('./assets/images/back1.svg'); background-image: url('./assets/images/back1.svg');
background-size: 100%; background-size: 100%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: bottom; background-position: bottom;
` `
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 52px; gap: 52px;
margin-bottom: 100px; margin-bottom: 100px;
` `
const ButtonWrapper = styled.div` const ButtonWrapper = styled.div`
display: flex; display: flex;
gap: 75px; gap: 75px;
flex-direction: column; flex-direction: column;
` `
const ButtonContainer = styled.div` const ButtonContainer = styled.div`
display: flex; display: flex;
gap: 15px; gap: 15px;
flex-direction: column; flex-direction: column;
` `
const Title = styled.div` const Title = styled.div`
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
gap: 10px; gap: 10px;
h1{ h1{
font-size: 52px; font-size: 52px;
}
`
export const HelloWindow = () => {
const { setWorkModule } = useWorkSpaceStore()
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
</Title>
<ButtonWrapper>
<ButtonContainer>
<MainButton
title="Настройки сети"
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title="О программе"
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
onClick={()=>setWorkModule(workSpaceNames[4])}
/>
</ButtonContainer>
</ButtonWrapper>
</Container>
</Wrapper>
)
} }
`
export const HelloWindow = () => {
const { setWorkModule } = useWorkSpaceStore()
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img src="./assets/images/logo.svg" alt="" style={{width: '152px'}}/>
</Title>
<ButtonWrapper>
<ButtonContainer>
<MainButton
title="Настройки сети"
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title="О программе"
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
onClick={()=>setWorkModule(workSpaceNames[4])}
/>
</ButtonContainer>
</ButtonWrapper>
</Container>
</Wrapper>
)
}

View file

@ -1,73 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const activeDevices = devices.filter(device => device.status === 'online');
const inactiveDevices = devices.filter(device => device.status !== 'online');
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
{devices.length > 0 ? (
<div style={{
display: 'flex',
width: '70%',
gap: '20px',
}}>
{/* Колонка активных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные устройства ({activeDevices.length})</h3>
{activeDevices.map((device, index) => (
<DeviceCard
key={index}
status="online"
device={device}
/>
))}
</div>
{/* Колонка неактивных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>Неактивные устройства ({inactiveDevices.length})</h3>
{inactiveDevices.map((device, index) => (
<DeviceCard
key={index}
status="offline"
device={device}
/>
))}
</div>
</div>
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -1,45 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
import TableScanner from "../../../../../UI/tables/TableScanner";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
{devices.length > 0 ? (
<TableScanner devices={devices} />
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -1,191 +0,0 @@
import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components";
import { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import ListUsers from "./modules/ListUsers";
import TableUsers from "./modules/TableUsers";
import DropdownType from "../../../../UI/Dropdown/DropdownType";
import useNetworkStore from "../../../../../store/useDeviceStore";
type Option ={
value: number,
label: string,
}
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:${({scanInitiated, loading})=>(scanInitiated && !loading ? 'start' : 'center')};
height: 100%;
width: 100%;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
position: relative;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
width: max-content;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: ${({scanInitiated,loading})=>(scanInitiated && !loading ? 'scale(0)' : 'scale(1)')};
span {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.accent};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
export default function UserScanner() {
const { ipAddress } = useIpStore();
const [loading, setLoading] = useState(false);
const { devices, setDevices } = useNetworkStore();
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
// Сканирование
const handleScan = async () => {
try {
setLoading(true);
setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress);
console.log(res);
setDevices(res.data.devices || []);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setDevices([])
} finally {
setLoading(false);
}
};
// Способ отображения
const viewOptions: Option[] = [
{ value: 1, label: 'Таблица' },
{ value: 2, label: 'Список' },
];
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
const handleViewChange = (option: Option) => {
setSelectedView(option);
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование пользователей в сети</TitleWrapper>)}
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
<span className="ring2"></span>
<span className="ring3"></span>
</UserLocate>
{scanInitiated && !loading && (
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
)}
{loading ? (
<LoadingText>
Сканирование сети<ScanningText />
</LoadingText>
) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
):(
<ListUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
)}
</Wrapper>
);
}

View file

@ -1,199 +0,0 @@
import styled, { keyframes } from "styled-components";
import React, { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIpStore} from "../../../../../store";
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:center ;
height: 100%;
width: 100%;
position: relative;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: scale(1);
.circel {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
const ScanBtn = styled.button<{scanInitiated: boolean}>`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: transform .4s ease;
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")};
&:hover{
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")} scale(1.03);
}
`
export function WifiScanner() {
const { ipAddress } = useIpStore();
const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const handleScan = async () => {
setWifiNetworks([]);
setCurrentNetwork(null)
setLoading(true);
setScanInitiated(true);
try {
const res = await NetworkService.scanWifi(ipAddress);
const networks = res.data.access_points || [];
setWifiNetworks(networks);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setWifiNetworks([]);
} finally {
setLoading(false);
}
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>Сканирование Wi-Fi сети</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span>
<span className="ring1 circel"></span>
<span className="ring2 circel"></span>
<span className="ring3 circel"></span>
{wifiNetworks.map((network) => (
<WifiDot
key={network.bssid}
network={network}
onSelect={setCurrentNetwork}
/>
))}
{currentNetwork && (
<ModalWifiScaner
onClose={() => setCurrentNetwork(null)}
network={currentNetwork}
/>
)}
</UserLocate>
{loading ? (
<LoadingText>
Сканирование сети <ScanningText/>
</LoadingText>
) : (
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать сеть'}
</ScanBtn>
)}
</Wrapper>
);
}

View file

@ -1,225 +0,0 @@
import React, {useEffect, useState} from 'react';
import styled from 'styled-components';
import {WifiNetworkScanner} from "./WifiDot";
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useIpStore, useModalContext} from "../../../../../../store";
import {Status} from "../../../../../customeNodes/nodes/networkInterface/networkNode/style";
import SignalSVG from "../../../../../UI/signalSvg";
import { useTheme } from 'styled-components';
import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore";
import {NetworkService} from "../../../../../../api/request";
const Modal = styled.div`
display: flex;
justify-content: center;
flex-direction: column;
background-color: ${(props) => props.theme.secondary};
border-radius: 24px;
padding: 32px 28px;
bottom: -40%;
left: 220px;
position: absolute;
z-index: 20;
min-width: 300px;
&:hover .close-btn{
opacity: 1;
visibility: visible;
}
`
const CloseBtn = styled.div`
position: absolute;
top: -20px;
right: -20px;
cursor: pointer;
background-color: ${(props) => props.theme.primary};
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
transition: all .4s ease;
opacity: 0;
visibility: visible;
&:hover {
background-color: ${(props) => props.theme.hoverBtn};
}
`
const ModalTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
line-height: 1;
margin-bottom: 32px;
h4{
font-weight: normal;
}
.functionalIcons{
display: flex;
justify-content: center;
align-items: center;
gap: 18px;
}
`
const ModalContent = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
justify-content: space-between;
align-items: center;
width: 100%;
`
const ModalRow = styled.div`
display: flex;
justify-content: space-between;
align-items: start;
gap: 12px;
width: 100%;
font-size: 14px;
h6{
margin: 0;
padding: 0;
font-size: 1em;
}
p{
max-width:180px;
text-align: right;
}
`
const ModalConnect = styled.button`
color: ${(props) => props.theme.primary};
padding: 10px 12px;
font-size: 18px;
background-color: ${(props) => props.theme.accent};
border-radius: 12px;
margin-top: 4px;
border: none;
outline: none;
width: 100%;
cursor: pointer;
transition: .4s ease;
&:hover{
background-color: ${(props) => props.theme.accentHover};
}
`
const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onClose: ()=>void }) => {
const { openModal } = useModalContext();
const { ipAddress } = useIpStore();
const theme = useTheme()
const [signal, setSignal] = useState(1);
const { wifiNetworks } = useNetworkStore();
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const getStatus = async () => {
try {
const response = await NetworkService.statusNetwork(ipAddress);
const { status, ssid } = response.data;
setIsConnected(
!!network?.ssid &&
ssid === network.ssid &&
status === 'connected'
);
} catch (error) {
setIsConnected(false);
}
};
getStatus();
}, [network?.ssid, ipAddress]);
const currentNetwork = wifiNetworks.find(n => n.bssid === network.bssid) || network;
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['7'], {
address: currentNetwork.ssid,
currentNote: currentNetwork.note || ''
});
};
const openModalNetwork= (e: React.MouseEvent) => {
e.stopPropagation();
if(isConnected){
openModal(ModalMap['9'], {
address: currentNetwork.ssid,
});
}else{
openModal(ModalMap['8'], {
address: currentNetwork.ssid,
});
}
};
useEffect(() => {
const perc = Number(network.signal);
switch (true) {
case perc >= -50: setSignal(5); break;
case perc >= -60: setSignal(4); break;
case perc >= -70: setSignal(3); break;
case perc >= -80: setSignal(2); break;
default: setSignal(1); break;
}
}, [network.signal])
return (
<Modal>
<CloseBtn className={'close-btn'} onClick={onClose}>
<IconComponent name={buttonNames[12]} width="30" height="30" />
</CloseBtn>
<ModalTitle>
<h4>{network.ssid}</h4>
<div className={'functionalIcons'}>
<span onClick={openModalNote} style={{cursor: "pointer"}}>
<IconComponent name={buttonNames[16]} width="30" height="30" />
</span>
<Status>
<SignalSVG signal={signal} size={'36px'} color={theme.accent} />
</Status>
</div>
</ModalTitle>
<ModalContent>
<ModalRow>
<h6>BSSID:</h6>
<p>{network.bssid}</p>
</ModalRow>
<ModalRow>
<h6>Производитель:</h6>
<p>{network.vendor}</p>
</ModalRow>
<ModalRow>
<h6>Сигнал:</h6>
<p>{network.signal} dBm</p>
</ModalRow>
<ModalRow>
<h6>Канал:</h6>
<p>{network.channel}</p>
</ModalRow>
<ModalRow>
<h6>Шифрование:</h6>
<p>{network.encryption}</p>
</ModalRow>
<ModalRow>
<h6>Частота:</h6>
<p>{network.frequency}</p>
</ModalRow>
<ModalRow>
<h6>Скрытая сеть:</h6>
<p>{network.hidden ? 'Да' : 'Нет'}</p>
</ModalRow>
<ModalRow>
<h6>Заметка:</h6>
<p>{currentNetwork.note ? currentNetwork.note : '-'}</p>
</ModalRow>
<ModalConnect onClick={openModalNetwork}>{isConnected ? 'Отключиться' : 'Подключиться'}</ModalConnect>
</ModalContent>
</Modal>
);
};
export default ModalWifiScaner;

View file

@ -1,79 +0,0 @@
import React from 'react';
import styled from "styled-components";
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
//Точка вайфай
const Dot = styled.div<{ top: string; left: string }>`
position: absolute;
top: ${props => props.top};
left: ${props => props.left};
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
transform: translate(-50%, -50%);
user-select: none;
transition: .4s;
text-wrap: nowrap;
&:hover {
transform: translate(-50%, -50%) scale(1.1);
}
`;
const WifiDot = ({ network, onSelect }: { network: WifiNetworkScanner, onSelect: (n: WifiNetworkScanner) => void }) => {
const getPositionInRing = (bssid: string, ringSize: number) => {
const hash = bssid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const randomAngle = (hash % 360) * (Math.PI / 180);
const innerRadius = ringSize * 0.4;
const outerRadius = ringSize * 0.5;
const randomValue = (hash % 1000) / 1000;
const randomRadius = Math.sqrt(innerRadius * innerRadius + randomValue * (outerRadius * outerRadius - innerRadius * innerRadius));
return {
x: Math.cos(randomAngle) * randomRadius,
y: Math.sin(randomAngle) * randomRadius
};
};
const getPosition = (signal: number, bssid: string) => {
let strength: 1 | 2 | 3;
if (signal >= -50) {
strength = 3;
} else if (signal >= -70) {
strength = 2;
} else if (signal >= -90) {
strength = 1;
} else {
strength = 1;
}
const ringSizes = { 1: 550, 2: 400, 3: 250 };
const ringSize = ringSizes[strength];
const offset = getPositionInRing(bssid, ringSize);
return {
top: `calc(50% - ${offset.y}px)`,
left: `calc(50% - ${offset.x}px)`
};
};
const position = getPosition(network.signal, network.bssid);
return (
<Dot top={position.top} left={position.left} onClick={() => onSelect(network)}>
{network.ssid}
</Dot>
);
};
export default WifiDot;

View file

@ -76,7 +76,7 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
setHasInited(false) setHasInited(false)
initializationAttempt.current = 0; initializationAttempt.current = 0;
const newSocket = new WebSocket(`ws://${ws}:8001/api/v1`); const newSocket = new WebSocket(`wss://${ws}:8001/api/v1`);
let connectionTimeout; let connectionTimeout;
setSocket(newSocket); setSocket(newSocket);
@ -192,13 +192,13 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
}, [nodes, loader]) }, [nodes, loader])
const onEdgesChange = useCallback( const onEdgesChange = useCallback(
(changes: any) => setEdges((eds) => applyEdgeChanges(changes, eds)), (changes: any) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges], [setEdges],
); );
const onConnect = useCallback( const onConnect = useCallback(
(connection: any) => setEdges((eds) => addEdge(connection, eds)), (connection: any) => setEdges((eds) => addEdge(connection, eds)),
[setEdges], [setEdges],
); );
const handleNodesChange = (changes: any) => { const handleNodesChange = (changes: any) => {
@ -254,29 +254,29 @@ export const NetworkInterfaces = ({ startConnection, ws }: Props) => {
}, [isNullPos]); }, [isNullPos]);
return ( return (
<> <>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onNodesChange={handleNodesChange} onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
style={{ background: '#F7F9FB' }} style={{ background: '#F7F9FB' }}
minZoom={0} minZoom={0}
maxZoom={4} maxZoom={4}
fitView fitView
fitViewOptions={{ padding: 0.1 }} fitViewOptions={{ padding: 0.1 }}
attributionPosition="bottom-left" attributionPosition="bottom-left"
snapToGrid snapToGrid
snapGrid={[20, 20]} snapGrid={[20, 20]}
panOnDrag={!isPanDisabled} panOnDrag={!isPanDisabled}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
proOptions={proOptions} proOptions={proOptions}
> >
<Background gap={20} size={1} bgColor={theme.background} /> <Background gap={20} size={1} bgColor={theme.background} />
<Controls fitViewOptions={{ padding: 0.1 }} /> <Controls fitViewOptions={{ padding: 0.1 }} />
</ReactFlow> </ReactFlow>
</> </>
); );
}; };

View file

@ -50,19 +50,10 @@ export const ToolBar = () => {
{toolBarMenu && toolBarMenu.map((menu) => ( {toolBarMenu && toolBarMenu.map((menu) => (
<ToolBarButton menuId={menu.id} key={menu.id} /> <ToolBarButton menuId={menu.id} key={menu.id} />
))} ))}
<Button>
<IconContainer>
<IconComponent name={buttonNames[14]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[5])} />
</IconContainer>
</Button>
<Button>
<IconContainer>
<IconComponent name={buttonNames[15]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[6])} />
</IconContainer>
</Button>
</ButtonContainer> </ButtonContainer>
<ButtonContainer> <ButtonContainer>
<Button> <Button>
<IconContainer> <IconContainer>
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} /> <IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} />

View file

@ -18,7 +18,7 @@ const Container = styled.div`
align-items: center; align-items: center;
gap: 15px; gap: 15px;
padding: 20px; padding: 20px;
width: 210px;
p{ p{
margin: 0; margin: 0;
} }
@ -115,12 +115,12 @@ export const DevicesBar = () => {
const checkWebSocketAvailability = async (ip: string): Promise<boolean> => { const checkWebSocketAvailability = async (ip: string): Promise<boolean> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const testSocket = new WebSocket(`ws://${ip}:8001/api/v1`); const testSocket = new WebSocket(`wss://${ip}:8001/api/v1`);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
testSocket.close(); testSocket.close();
resolve(false); resolve(false);
}, 500 }, 500
); );
testSocket.onopen = () => { testSocket.onopen = () => {
clearTimeout(timeout); clearTimeout(timeout);
@ -196,12 +196,12 @@ export const DevicesBar = () => {
<DeviceBlock> <DeviceBlock>
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}> <DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
<p>Корневой адрес</p> <p>Текущее устройство</p>
</DeviceControl> </DeviceControl>
{currentDevice.hostname == '' ? ( {currentDevice.hostname == '' ? (
<Text className="paddingP">Название: Загрузка<AnimDot /></Text> <Text className="paddingP">Адрес: Загрузка<AnimDot /></Text>
) : ( ) : (
<p className="paddingP">Название: {currentDevice.hostname}</p> <p className="paddingP">Адрес: {currentDevice.hostname}</p>
)} )}
</DeviceBlock> </DeviceBlock>

View file

@ -3,15 +3,11 @@ import { useToolBar } from "../../../../hooks/useToolBar"
import { Space } from "../../../../const/menus/sideBar/components" import { Space } from "../../../../const/menus/sideBar/components"
import styled from "styled-components" import styled from "styled-components"
const Container = styled.div<{isOpen: boolean}>` const Container = styled.div`
background-color: ${(props) => props.theme.secondary}; background-color: ${(props) => props.theme.secondary};
width: 250px; width: 250px;
font-size: 14px; font-size: 14px;
flex: 3; flex: 3;
overflow: hidden;
max-width: ${({isOpen})=>(isOpen ? '250px' : '0')};
transition: max-width .4s ease;
` `
export const ToolSpace = () => { export const ToolSpace = () => {
@ -19,12 +15,12 @@ export const ToolSpace = () => {
const name = toolBar?.current || '' const name = toolBar?.current || ''
const { isActive, isOpen } = useToolBar(name) const { isActive, isOpen } = useToolBar(name)
if (!isActive) { if (!isActive || !isOpen) {
return null return null
} }
return ( return (
<Container isOpen={isOpen}> <Container>
<Space name={name} /> <Space name={name} />
</Container> </Container>
) )

View file

@ -9,8 +9,6 @@ import { workSpaceNames } from "../../const/workspace/names";
import { Header } from "./modules/appModules/header"; import { Header } from "./modules/appModules/header";
import { ReactFlowProvider } from "@xyflow/react"; import { ReactFlowProvider } from "@xyflow/react";
import About from "./modules/appModules/about"; import About from "./modules/appModules/about";
import UserScanner from "./modules/appModules/usersScanner/userScanner";
import {WifiScanner} from "./modules/appModules/wifiScanner/WifiScanner";
const Section = styled.div` const Section = styled.div`
display: flex; display: flex;
@ -77,12 +75,6 @@ export default function WorkSpace() {
from={"helloWindow"} from={"helloWindow"}
/> />
)} )}
{workModule === workSpaceNames[5] && (
<UserScanner />
)}
{workModule === workSpaceNames[6] && (
<WifiScanner />
)}
{workModule === workSpaceNames[3] && {workModule === workSpaceNames[3] &&
<WorkSection> <WorkSection>
<Header /> <Header />

View file

@ -12,13 +12,10 @@ import arrowRight from '@iconify/icons-ic/baseline-keyboard-arrow-right';
import edit from '@iconify/icons-ic/baseline-edit'; import edit from '@iconify/icons-ic/baseline-edit';
import checkCircle from '@iconify/icons-ic/round-check-circle'; import checkCircle from '@iconify/icons-ic/round-check-circle';
import cancel from '@iconify/icons-ic/baseline-cancel'; import cancel from '@iconify/icons-ic/baseline-cancel';
import userSearch from '@iconify/icons-ic/baseline-person-search';
import admin from '@iconify/icons-ic/baseline-admin-panel-settings'; import admin from '@iconify/icons-ic/baseline-admin-panel-settings';
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator' import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
import refresh from '@iconify/icons-ic/baseline-refresh'; import refresh from '@iconify/icons-ic/baseline-refresh';
import close from '@iconify/icons-ic/round-close'; import close from '@iconify/icons-ic/round-close';
import wifiFind from '@iconify/icons-ic/baseline-wifi-find';
import noteEdit from '@iconify/icons-ph/note-pencil';
export const ICONS = { export const ICONS = {
[buttonNames[1]]: exitToApp, [buttonNames[1]]: exitToApp,
@ -33,10 +30,8 @@ export const ICONS = {
[buttonNames[10]]: admin, [buttonNames[10]]: admin,
[buttonNames[11]]: refresh, [buttonNames[11]]: refresh,
[buttonNames[12]]: close, [buttonNames[12]]: close,
[buttonNames[13]]: noAdmin, [buttonNames[13]]: noAdmin
[buttonNames[14]]: userSearch,
[buttonNames[15]]: wifiFind,
[buttonNames[16]]: noteEdit,
} as const; } as const;
type IconName = keyof typeof ICONS; type IconName = keyof typeof ICONS;

View file

@ -13,8 +13,5 @@ export const buttonNames: names ={
10: 'admin', 10: 'admin',
11: 'reset', 11: 'reset',
12: 'close', 12: 'close',
13: 'noAdmin', 13: 'noAdmin'
14: 'userSearch',
15: 'wifiFind',
16: 'noteEdit'
} }

View file

@ -1,7 +1,5 @@
const baseTheme = { const baseTheme = {
accent: '#007ACC', accent: '#007ACC',
successColor: "#4CAf50",
errorColor: "#F44336",
accentHover: '#4A90E2', accentHover: '#4A90E2',
} }
@ -11,10 +9,6 @@ export const light = {
secondary: '#EDEDED', secondary: '#EDEDED',
background: '#E8E8E8', background: '#E8E8E8',
textColor: '#000000', textColor: '#000000',
borderColor: '#404040',
scannerColor: '#007ACC',
offlineColor: '#ababab',
hoverBtn: '#d6d6d6',
}; };
export const dark = { export const dark = {
@ -23,8 +17,4 @@ export const dark = {
secondary: '#252525', secondary: '#252525',
background: '#3A3A3A', background: '#3A3A3A',
textColor: '#FFFFFF', textColor: '#FFFFFF',
borderColor: '#b6b6b6',
scannerColor: '#0E9C9D',
offlineColor: '#656565',
hoverBtn: '#4a4a4a',
}; };

View file

@ -4,7 +4,5 @@ export const workSpaceNames: names ={
1: 'helloWindow', 1: 'helloWindow',
2: 'aboutFromWork', 2: 'aboutFromWork',
3: 'networkInterfaces', 3: 'networkInterfaces',
4: 'aboutFromHello', 4: 'aboutFromHello'
5: 'userScanner',
6: 'wifiScanner',
} }

View file

@ -9,7 +9,7 @@ interface IpAddressState {
export const useIpStore = create<IpAddressState>()( export const useIpStore = create<IpAddressState>()(
persist( persist(
(set)=>({ (set)=>({
ipAddress: '192:168.1.147', ipAddress: 'localhost',
setIpAddress: (newIp: string) => set({ ipAddress: newIp }), setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
}), }),
{ {

View file

@ -1,51 +0,0 @@
import { create } from 'zustand';
export interface DeviceUser {
ip: string;
mac: string;
hostname: string;
vendor: string;
open_ports: string[];
status: string;
last_seen: string;
note: string;
}
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
interface NetworkStore {
devices: DeviceUser[];
setDevices: (devices: DeviceUser[]) => void;
addDevice: (device: DeviceUser) => void;
wifiNetworks: WifiNetworkScanner[];
setWifiNetworks: (networks: WifiNetworkScanner[]) => void;
addWifiNetwork: (network: WifiNetworkScanner) => void;
clearAll: () => void;
}
const useNetworkStore = create<NetworkStore>((set) => ({
devices: [],
setDevices: (devices) => set({ devices }),
addDevice: (device) =>
set((state) => ({ devices: [...state.devices, device] })),
wifiNetworks: [],
setWifiNetworks: (networks) => set({ wifiNetworks: networks }),
addWifiNetwork: (network) =>
set((state) => ({ wifiNetworks: [...state.wifiNetworks, network] })),
clearAll: () => set({ devices: [], wifiNetworks: [] })
}));
export default useNetworkStore;

View file

@ -1,5 +1,6 @@
import { useIpStore } from "../store"; import { useIpStore } from "../store";
import { useAdminStore } from "../store/adminStore"; import { useAdminStore } from "../store/adminStore";
import { useEffect } from "react";
import { NetworkService } from "../api/request"; import { NetworkService } from "../api/request";
export const useCheckAdmin = () => { export const useCheckAdmin = () => {

View file

@ -1,61 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import { useIpStore } from './src/store';
import { useEffect } from 'react';
import { useCheckAdmin } from './src/utils/checkAdmin';
import {
ModalProvider,
useModalContext,
ThemeProvider
} from './src/store';
import WindowComponent from './src/components/modalwindows/modalWindow';
import Router from './src/router';
import { I18nProvider } from "./src/utils/i18n/provider";
import { useLanguage } from "./src/hooks/useSettings";
const AppWithProviders = () => {
const currentLanguage = useLanguage();
return (
<I18nProvider initialLanguage={currentLanguage}>
<ThemeProvider>
<HashRouter>
<ModalProvider>
<MainComponent />
</ModalProvider>
</HashRouter>
</ThemeProvider>
</I18nProvider>
);
};
const MainComponent = () => {
const checkAdmin = useCheckAdmin();
const { ipAddress } = useIpStore();
useEffect(() => {
const checkAdminStatus = async () => {
await checkAdmin();
};
checkAdminStatus();
}, [ipAddress]);
const { isOpen, modalType, modalData } = useModalContext();
return (
<>
<WindowComponent isOpen={isOpen} type={modalType} data={modalData} />
<Router />
</>
);
};
const container = document.getElementById('root');
if (!container) {
throw new Error('Root element with id "root" not found');
}
const root = createRoot(container);
root.render(<AppWithProviders />);

File diff suppressed because it is too large Load diff

View file

@ -1,11 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => ipcRenderer.send(channel, data),
on: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
quitApp: () => ipcRenderer.send('quit-app'),
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
writeFile: (path, data) => ipcRenderer.invoke('write-file', { path, data }),
onThemeChange: (callback) => ipcRenderer.on('theme-update', (event, theme) => callback(theme))
});

View file

@ -1,186 +0,0 @@
{
"userScanner-title": "Subnet Node Scanning",
"userScanner-visible": "Display Method",
"userScanner-loading": "Scanning nodes",
"userScanner-refresh": "Refresh List",
"userScanner-scan": "Scan Nodes",
"userScanner-notFound": "Nodes not found",
"tableScanner-online": "Online",
"tableScanner-name": "Name",
"tableScanner-vendor": "Vendor",
"tableScanner-ip": "IP Address",
"tableScanner-ports": "Open Ports",
"tableScanner-activity": "Last Activity",
"tableScanner-note": "Note",
"tableScanner-action": "Action",
"tableScanner-unknown": "Unknown",
"tableScanner-noData": "No data to display",
"listUsers-activeNodes": "Active Nodes",
"listUsers-inactiveNodes": "Inactive Nodes",
"ipInput-placeholder": "Enter IP",
"ipInput-error": "Enter a valid IP address",
"userScanner-visible_table": "Table",
"userScanner-visible_list": "List",
"wifiScanner-title": "Wi-Fi Access Point Scanning",
"wifiScanner-scanning": "Scanning access points",
"wifiScanner-scanError": "Network scanning error",
"wifiScanner-refresh": "Refresh List",
"wifiScanner-scan": "Scan Access Points",
"wifiModal-bssid": "BSSID",
"wifiModal-vendor": "Vendor",
"wifiModal-signal": "Signal",
"wifiModal-channel": "Channel",
"wifiModal-encryption": "Encryption",
"wifiModal-frequency": "Frequency",
"wifiModal-hidden": "Hidden Network",
"wifiModal-note": "Note",
"wifiModal-connect": "Connect",
"wifiModal-disconnect": "Disconnect",
"wifiModal-unknown": "Unknown",
"wifiModal-none": "None",
"common-yes": "Yes",
"common-no": "No",
"addNode": "Add Node",
"enterNodeAddress": "Enter node address...",
"connectionLost": "Connection Lost",
"serverDisconnected": "Server connection interrupted",
"checkAddressAndRetry": "Please check the address availability and try again",
"cancel": "Cancel",
"close": "Close",
"save": "Save",
"retry": "Retry",
"connect": "Connect",
"submit": "Submit",
"common-back": "Back",
"error": "Error",
"networkConnection": "Network Connection",
"enterPassword": "Enter password...",
"connectingAttempt": "Attempting to connect to the network",
"connectionError": "Network connection error",
"connectionErrorLog": "Connection error: ",
"customizeTable-title": "Subnet Node Table Customization",
"wifiDisconnect-title": "Wi-Fi Network Disconnection",
"wifiDisconnect-confirm": "Confirm Disconnection",
"wifiDisconnect-message1": "You are about to disconnect from the network - ",
"wifiDisconnect-message2": " All active connections will be interrupted.",
"wifiDisconnect-disconnect": "Disconnect",
"wifiDisconnect-progress": "Disconnecting from the network",
"ipChange-title": "IP Address Change",
"ipChange-checkingIp": "Checking IP address validity",
"ipChange-generalInfo": "General Information",
"ipChange-interface": "Interface",
"ipChange-ipInfo": "IP Information",
"ipChange-currentIp": "Current IP",
"ipChange-currentGateway": "Current Gateway",
"ipChange-currentDns": "Current DNS",
"ipChange-newIp": "New IP",
"ipChange-newGateway": "New Gateway",
"ipChange-newDns": "New DNS",
"ipChange-checkError": "An error occurred while checking validity",
"ipChange-adminRequired": "To change network settings, you must log in as an administrator",
"note-edit": "Edit Note",
"note-add": "Add Note",
"note-enter": "Enter note...",
"auth-successLogin": "Successful Login",
"auth-successLogout": "Successful Logout",
"auth-getAccess": "Obtaining Access Rights",
"auth-enableAdmin": "Enable Administrative Access",
"auth-enterAdminPass": "Enter administrator password...",
"auth-wrongPass": "Incorrect operator password",
"auth-switchToLimited": "Switch to Limited Access",
"auth-limitedModeDesc": "In this mode, administrator privileges are restricted.",
"auth-restrictAccess": "Restrict Access",
"auth-closeHint": "This window can be closed, or it will disappear on its own",
"operatorConfig-title": "Node Operator Configuration",
"operatorConfig-instruction": "Before starting, you need to create an application operator password",
"operatorConfig-passwordPlaceholder": "Enter password...",
"operatorConfig-repeatPasswordPlaceholder": "Repeat password...",
"operatorConfig-passwordMismatch": "Passwords do not match!",
"operatorConfig-submit": "Submit",
"about-logoAlt": "Gossys Logo",
"about-title": "About the Application",
"about-description": "GoNets is a minimally viable cross-platform product (MVP) for demonstrating our approach to managing PC settings in a corporate environment.",
"about-featuresTitle": "Features",
"about-feature1": "Visualization of active network resources",
"about-feature2": "Configuration of local and remote network resource parameters",
"about-feature3": "Management of resource interaction with the GoNets Client",
"about-disclaimer": "The software is for demonstration and home use only.",
"about-goToPro": "Go to GoNets-PRO",
"about-homepage": "Homepage",
"about-documentation": "Documentation",
"about-license": "License",
"about-copyright": "Developed and maintained by Hybrid Office Systems LLC, 2024-2025. All rights reserved.",
"main-logoAlt": "Application Logo",
"main-networkSettings": "Network Settings",
"main-about": "About the Program",
"nodes-availableNodes": "Available Nodes",
"nodes-rootAddress": "Root Address",
"nodes-name": "Name",
"nodes-loading": "Loading",
"nodes-additionalNodes": "Additional Nodes",
"nodes-node": "Node",
"nodes-address": "Address",
"nodes-connectionError": "Failed to connect to the server at the address ",
"settings-nodesSection": "Subnet Nodes",
"settings-intervalScanning": "Interval Scanning",
"settings-scanInterval": "Node Scan Interval (min)",
"settings-displayNames": "Display Names When Missing",
"settings-ipAddress": "IP Address",
"settings-unknown": "Unknown",
"settings-rowsPerPage": "Rows Per Table",
"settings-displaySettings": "Nodes Display",
"settings-configure": "Configure",
"settings-clearSessionData": "Clear Session Data",
"settings-clear": "Clear",
"settings-clearFailed": "Data was not deleted: ",
"settings-exportData": "Export Nodes Data",
"settings-export": "Export",
"settings-saveNodesTitle": "Save Nodes Data",
"settings-exportSuccess": "Data exported successfully!",
"settings-exportError": "Export error: ",
"settings-status": "Online",
"settings-name": "Name",
"settings-vendor": "Manufacturer",
"settings-openPorts": "Open Ports",
"settings-lastActivity": "Last Activity",
"settings-note": "Note",
"settings-action": "Action",
"settings-title": "Settings",
"settings-resetSettings": "Reset Settings",
"settings-resetConfirm": "Are you sure you want to reset all settings to default values?",
"settings-cancel": "Cancel",
"settings-save": "Save",
"settings-fixErrors": "Fix errors before saving",
"settings-fillRequired": "Fill in all required fields",
"settings-dataNotDeleted": "Data was not deleted: ",
"settings-saveNetworksTitle": "Save Networks Data",
"settings-fieldRequired": "Field cannot be empty",
"settings-enterNumber": "Please enter a number",
"settings-valueTooSmall": "Value cannot be less than ",
"settings-valueTooLarge": "Value cannot be greater than ",
"settings-sections-subnetNodes": "Subnet Nodes",
"settings-sections-accessPoints": "Access Points",
"settings-sections-appearance": "Appearance",
"settings-exportNetworkData": "Export Access Points Data",
"settings-colorTheme": "Color Theme",
"settings-light": "Light",
"settings-dark": "Dark",
"settings-system": "As in System",
"settings-interfaceLanguage": "Interface Language",
"settings-russian": "Russian",
"settings-english": "English"
}

View file

@ -1,192 +0,0 @@
{
"userScanner-title": "Сканирование узлов подсети",
"userScanner-visible": "Способ отображения",
"userScanner-loading": "Cканирование узлов",
"userScanner-refresh": "Обновить список",
"userScanner-scan": "Сканировать узлы",
"userScanner-notFound": "Узлы не найдены",
"tableScanner-online": "Онлайн",
"tableScanner-name": "Имя",
"tableScanner-vendor": "Производитель",
"tableScanner-ip": "IP адрес",
"tableScanner-ports": "Открытые порты",
"tableScanner-activity": "Последняя активность",
"tableScanner-note": "Заметка",
"tableScanner-action": "Действие",
"tableScanner-unknown": "Неизвестно",
"tableScanner-noData": "Нет данных для отображения",
"listUsers-activeNodes": "Активные узлы",
"listUsers-inactiveNodes": "Неактивные узлы",
"ipInput-placeholder": "Введите IP",
"ipInput-error": "Введите корректный IP-адрес",
"userScanner-visible_table": "Таблица",
"userScanner-visible_list": "Список",
"wifiScanner-title": "Сканирование точек доступа Wi-Fi",
"wifiScanner-scanning": "Сканирование точек доступа",
"wifiScanner-scanError": "Ошибка сканирования сети",
"wifiScanner-refresh": "Обновить список",
"wifiScanner-scan": "Сканировать точки доступа",
"wifiModal-bssid": "BSSID",
"wifiModal-vendor": "Производитель",
"wifiModal-signal": "Сигнал",
"wifiModal-channel": "Канал",
"wifiModal-encryption": "Шифрование",
"wifiModal-frequency": "Частота",
"wifiModal-hidden": "Скрытая сеть",
"wifiModal-note": "Заметка",
"wifiModal-connect": "Подключиться",
"wifiModal-disconnect": "Отключиться",
"wifiModal-unknown": "Неизвестно",
"wifiModal-none": "Отсутствует",
"common-yes": "Да",
"common-no": "Нет",
"addNode": "Добавление узла",
"enterNodeAddress": "Введите адрес узла...",
"connectionLost": "Соединение прервано",
"serverDisconnected": "Произошёл разрыв с сервером",
"checkAddressAndRetry": "Пожалуйста, проверьте доступ к адресу и попробуйте снова",
"cancel": "Отменить",
"close": "Закрыть",
"save": "Сохранить",
"retry": "Повторить",
"connect": "Подключиться",
"common-back": "Назад",
"submit": "Подтвердить",
"error": "Ошибка",
"networkConnection": "Подключение к сети",
"enterPassword": "Введите пароль...",
"connectingAttempt": "Попытка подключения к сети",
"connectionError": "Ошибка подключения к сети",
"connectionErrorLog": "Ошибка подключения: ",
"customizeTable-title": "Кастомизация таблицы узлов подсети",
"wifiDisconnect-title": "Отключение от Wi-Fi сети",
"wifiDisconnect-confirm": "Подтверждение отключения",
"wifiDisconnect-message1": "Вы собираетесь отключиться от сети - ",
"wifiDisconnect-message2": " Все активные подключения будут прерваны.",
"wifiDisconnect-disconnect": "Отключиться",
"wifiDisconnect-progress": "Отключение от сети",
"ipChange-title": "Изменение IP-Адреса",
"ipChange-checkingIp": "Идет проверка корректности IP адреса",
"ipChange-generalInfo": "Общие сведения",
"ipChange-interface": "Интерфейс",
"ipChange-ipInfo": "Сведения об IP",
"ipChange-currentIp": "Текущий ip",
"ipChange-currentGateway": "Текущий шлюз",
"ipChange-currentDns": "Текущий DNS",
"ipChange-newIp": "Новый ip",
"ipChange-newGateway": "Новый шлюз",
"ipChange-newDns": "Новый DNS",
"ipChange-checkError": "Произошла ошибка проверки корректности",
"ipChange-adminRequired": "Для изменения сетевых настроек необходимо авторизироваться под администратором",
"note-edit": "Редактирование заметки",
"note-add": "Добавление заметки",
"note-enter": "Введите заметку...",
"auth-successLogin": "Успешный вход",
"auth-successLogout": "Успешный выход",
"auth-getAccess": "Получение прав доступа",
"auth-enableAdmin": "Включить административный доступ",
"auth-enterAdminPass": "Введите пароль администратора...",
"auth-wrongPass": "Неверный пароль оператора",
"auth-switchToLimited": "Переключиться на ограниченный доступ",
"auth-limitedModeDesc": "В данном режиме доступа ограничиваются полномочия администратора.",
"auth-restrictAccess": "Ограничить доступ",
"auth-closeHint": "Это окно можно закрыть, либо оно пропадёт само",
"operatorConfig-title": "Настройка оператора узла",
"operatorConfig-instruction": "Перед началом работы необходимо создать пароль оператора приложения",
"operatorConfig-passwordPlaceholder": "Введите пароль...",
"operatorConfig-repeatPasswordPlaceholder": "Повторите пароль...",
"operatorConfig-passwordMismatch": "Пароли не совпадают!",
"operatorConfig-submit": "Отправить",
"about-logoAlt": "Логотип Gossys",
"about-title": "О приложении",
"about-description": "ГоNets - минимально работоспособный кроссплатформенный продукт (MVP) для демонстрации нашего подхода к управлению настройками ПК в корпоративной среде.",
"about-featuresTitle": "Возможности",
"about-feature1": "Визуализация активных сетевых ресурсов",
"about-feature2": "Настройка параметров локальных и удалённых сетевых ресурсов",
"about-feature3": "Управление взаимодействием ресурсов с Клиентом ГoNets",
"about-disclaimer": "Программное обеспечение исключительно для ознакомления и домашнего использования.",
"about-goToPro": "Перейти на ГоNets-PRO",
"about-homepage": "Домашняя страница",
"about-documentation": "Документация",
"about-license": "Лицензия",
"about-copyright": "Разработано и поддерживается ООО «Гибридные Офисные Системы» 2024-2025 г. Все права защищены.",
"main-logoAlt": "Логотип приложения",
"main-networkSettings": "Настройки сети",
"main-about": "О программе",
"nodes-availableNodes": "Доступные узлы",
"nodes-rootAddress": "Корневой адрес",
"nodes-name": "Имя",
"nodes-loading": "Загрузка",
"nodes-additionalNodes": "Дополнительные узлы",
"nodes-node": "Узел",
"nodes-address": "Адрес",
"nodes-connectionError": "Не удалось подключиться к серверу по адресу ",
"settings-nodesSection": "Узлы подсети",
"settings-intervalScanning": "Интервальное сканирование",
"settings-scanInterval": "Интервал сканирования узлов (мин)",
"settings-displayNames": "Отображаемые имена при отсутствии",
"settings-ipAddress": "IP адрес",
"settings-unknown": "Неизвестно",
"settings-rowsPerPage": "Количество строк в таблице",
"settings-displaySettings": "Отображение узлов",
"settings-configure": "Настроить",
"settings-clearSessionData": "Очистить данные сессии",
"settings-clear": "Очистить",
"settings-clearFailed": "Данные не были удалены: ",
"settings-exportData": "Экспорт данных узлов",
"settings-export": "Экспортировать",
"settings-saveNodesTitle": "Сохранить данные узлов",
"settings-exportSuccess": "Данные успешно экспортированы!",
"settings-exportError": "Ошибка экспорта: ",
"settings-status": "Онлайн",
"settings-name": "Имя",
"settings-vendor": "Производитель",
"settings-openPorts": "Открытые порты",
"settings-lastActivity": "Последняя активность",
"settings-note": "Заметка",
"settings-action": "Действие",
"settings-title": "Настройки",
"settings-resetSettings": "Сбросить настройки",
"settings-resetConfirm": "Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?",
"settings-cancel": "Отменить",
"settings-save": "Сохранить",
"settings-fixErrors": "Исправьте ошибки перед сохранением",
"settings-fillRequired": "Заполните все обязательные поля",
"settings-dataNotDeleted": "Данные не были удалены: ",
"settings-saveNetworksTitle": "Сохранить данные сетей",
"settings-fieldRequired": "Поле не может быть пустым",
"settings-enterNumber": "Пожалуйста, введите число",
"settings-valueTooSmall": "Значение не может быть меньше ",
"settings-valueTooLarge": "Значение не может быть больше ",
"settings-sections-subnetNodes": "Узлы подсети",
"settings-sections-accessPoints": "Точки доступа",
"settings-sections-appearance": "Внешний вид",
"settings-exportNetworkData": "Экспорт данных точек доступа",
"settings-colorTheme": "Цветовая тема",
"settings-light": "Светлая",
"settings-dark": "Тёмная",
"settings-system": "Как в системе",
"settings-interfaceLanguage": "Язык интерфейса",
"settings-russian": "Русский",
"settings-english": "Английский"
}

View file

@ -1,196 +0,0 @@
import axios, { AxiosResponse } from "axios";
const axiosWithoutCredentials = axios.create({
baseURL: `http://localhost:8001`,
})
export class NetworkService {
static createAxiosInstance(ipAddress: string) {
return axios.create({
baseURL: `http://${ipAddress}:8001`,
});
}
static async checkIp(ipAddress: string, data: any): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/ip/check_ip', data);
} catch (error: any) {
return error.response;
}
}
static async saveIp(ipAddress: string, data: any): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/ip/apply_ip', data);
} catch (error: any) {
return error.response;
}
}
static async getHostname(): Promise<AxiosResponse>{
try{
const response = await axiosWithoutCredentials.get('/network/get_hostname')
return response
} catch (error: any){
return error.response
}
}
static async createUser(ipAddress: string, data: any): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/auth/register', data);
} catch (error: any) {
return error.response;
}
}
static async login(ipAddress: string, data: any): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/auth/login', data);
} catch (error: any) {
return error.response;
}
}
static async logout(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/auth/logout');
} catch (error: any) {
return error.response;
}
}
static async checkServerConf(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/auth/check_json');
} catch (error: any) {
return error.response;
}
}
static async whoami(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/auth/me');
} catch (error: any) {
return error.response;
}
}
static async scanUser(ipAddress: string, ipScanning: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post('/subnet/devices',{
subnet: ipScanning,
});
} catch (error: any) {
return error.response;
}
}
static async scanWifi(ipAddress: string): Promise<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
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<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/wifi/disconnect`, {
ssid: ssid,
});
} catch (error: any) {
return error.response;
}
}
// Settings
static async clearSubnet(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/clear_subnet`);
} catch (error: any) {
return error.response;
}
}
static async clearNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/clear_wifi`);
} catch (error: any) {
return error.response;
}
}
static async exportDevices(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export_devices`);
} catch (error: any) {
return error.response;
}
}
static async exportNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/settings/export_wifi`);
} catch (error: any) {
return error.response;
}
}
}

View file

@ -1,138 +0,0 @@
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";
import {useNameSetting} from "../../../hooks/useSettings";
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<DeviceCardProps> = ({ device, status }) => {
const [isExpanded, setIsExpanded] = useState(false);
const { openModal } = useModalContext();
const nameSetting = useNameSetting();
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
return (
<CardWrapper status={status} onClick={()=>setIsExpanded(!isExpanded)}>
<CardHeader status={status}>
<p>
MAC: {device.mac}
</p>
<span onClick={openModalNote}>
<IconComponent name={buttonNames[16]} width="28" height="28" setColor={'white'} />
</span>
</CardHeader>
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || (nameSetting === 'ip' ? device.ip : 'Неизвестно')}</Value>
</InfoRow>
<InfoRow>
<Label>Производитель:</Label>
<Value>{device.vendor || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>IP адрес:</Label>
<Value>{device.ip}</Value>
</InfoRow>
{device.open_ports?.length > 0 && (
<InfoRow>
<Label>Открытые порты:</Label>
<Value>{device.open_ports.join(', ')}</Value>
</InfoRow>
)}
<InfoRow>
<Label>Последняя активность:</Label>
<Value>{device.last_seen}</Value>
</InfoRow>
<InfoRow>
<Label>Заметка:</Label>
<Value>{device.note || '-'}</Value>
</InfoRow>
</CardContent>
</CardWrapper>
);
};
export default DeviceCard;

View file

@ -1,130 +0,0 @@
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<HTMLDivElement>(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 (
<DropdownWrapper ref={dropdownRef}>
<Label>{label}</Label>
<Dropdown>
<DropdownHeader onClick={toggleOpen}>
<p>{value?.label || 'Select an option'}</p>
<DropdownArrow $isOpen={isOpen}>
<IconComponent name={buttonNames[6]} />
</DropdownArrow>
</DropdownHeader>
<DropdownContent $isOpen={isOpen}>
{options.map((option, i) => (
<DropdownRow key={i} onClick={() => handleSelect(option)}>
{option.label}
</DropdownRow>
))}
</DropdownContent>
</Dropdown>
</DropdownWrapper>
);
};
export default DropdownType;

View file

@ -1,77 +0,0 @@
import React from 'react';
import styled from "styled-components";
import {buttonNames} from "../../const/buttons/names";
import {IconComponent} from "../../const/buttons/icons";
const PaginateWrapper = styled.div`
width: 100%;
margin-top: 24px;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
`
const Page = styled.button<{ active?: boolean }>`
width: 36px;
height: 36px;
background: ${(props) => props.active ? props.theme.borderColor : props.theme.primary};
color: ${(props) => props.active ? props.theme.primary : props.theme.textColor};
border: 1px solid ${(props => props.theme.borderColor)};
font-size: 16px;
cursor: pointer;
border-radius: 4px;
transition: .4s ease;
&:hover {
background: ${(props) => props.active ? props.theme.textColor : props.theme.background};;
}
`
interface PaginationProps {
currentPage: number;
totalPages: number;
onPrevPage: () => void;
onNextPage: () => void;
onPageChange: (page: number) => void;
}
const Pagination = ({currentPage, totalPages, onPrevPage, onNextPage, onPageChange}: PaginationProps) => {
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
return (
<PaginateWrapper>
<Page
onClick={onPrevPage}
disabled={currentPage === 1}
>
<IconComponent
width="20"
height="20"
name={buttonNames[17]}
/>
</Page>
{pageNumbers.map((page, i) => (
<Page
key={i}
onClick={() => onPageChange(page)}
active={currentPage === page}
>
{page}
</Page>
))}
<Page
onClick={onNextPage}
disabled={currentPage === totalPages}
>
<IconComponent
width="20"
height="20"
name={buttonNames[6]}
/>
</Page>
</PaginateWrapper>
);
};
export default Pagination;

View file

@ -1,87 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
onClose: () => void;
}
export const ConnectNetwork = ({address, onClose}: Props) => {
const [password, setPassword] = useState('');
const theme = useTheme();
const {ipAddress} = useIpStore();
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleSave = async () => {
try {
setIsLoading(true);
setError('');
const res = await NetworkService.connectNetwork(ipAddress, password, address);
if (res.status === 200) {
onClose();
} else {
setError(res.data.detail);
}
} catch (error) {
console.error(t('connectionErrorLog'), error);
setError(t('connectionError'));
} finally {
setIsLoading(false);
}
};
return (
<ModalContainer style={{height: '230px'}}>
<ModalTitle>
<h3>{t('networkConnection')}</h3>
</ModalTitle>
<ModalContent style={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
<Input
type="password"
placeholderText={t('enterPassword')}
value={password}
id='deviceNote'
onChange={(value) => setPassword(value)}
/>
{error && (
<p style={{color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right'}}>
{error}
</p>
)}
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="button"
size="small"
color={theme.successColor}
hoverColor={theme.successColor}
title={t('connect')}
onClick={handleSave}
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText={t('connectingAttempt')}/>}
</ModalContainer>
);
};

View file

@ -1,83 +0,0 @@
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {useIpStore} from "../../../store";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { devices, setDevices } = useNetworkStore();
const {t} = useTranslation('common');
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedDevices = devices.map(device =>
device.mac === address
? { ...device, note: note.trim() }
: device
);
setDevices(updatedDevices);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '250px' }}>
<ModalTitle>
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText={t('note-enter')}
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title={t('save')}
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -1,84 +0,0 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useIpStore} from "../../../store";
import {useTranslation} from "../../../utils/i18n/hooks";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const {t} = useTranslation('common');
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedNetworks = wifiNetworks.map(network =>
network.ssid === address
? { ...network, note: note.trim() }
: { ...network }
);
setWifiNetworks(updatedNetworks);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '220px' }}>
<ModalTitle>
<h3>{currentNote ? t('note-edit') : t('note-add')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText={t('note-enter')}
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('cancel')}
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title={t('save')}
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -1,143 +0,0 @@
import React, {useEffect, useState} from 'react';
import {ButtonContainer, ModalContainer, ModalContent, ModalTitle} from "../style";
import {MainButton} from "../../UI/buttons/mainButton";
import {ModalMap} from "../windowsType";
import {useModalContext} from "../../../store";
import styled from "styled-components";
import { useSettingsStore } from '../../../store/settings/settingsStore';
import {useTranslation} from "../../../utils/i18n/hooks";
const StyledTableList = styled.ul`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 0 40px;
list-style-type: none;
`
const StyledTableItem = styled.li`
width: 160px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
background-color: ${props => props.theme.accent};
color: ${props => props.theme.primary};
border: 1px solid ${props => props.theme.primary};
&:first-child {
border-radius: 16px 0 0 16px;
}
&:last-child {
border-radius: 0 16px 16px 0;
}
&.off{
background-color: ${props => props.theme.offlineColor};
}
`
const CustomizeNodes = () => {
const { openModal } = useModalContext();
const pendingChanges = useSettingsStore(state => state.pendingChanges);
const updateSetting = useSettingsStore(state => state.updateSetting);
const getSetting = useSettingsStore(state => state.getSetting);
const initializeSettings = useSettingsStore(state => state.initializeSettings);
const {t} = useTranslation('common');
const [dragIndex, setDragIndex] = useState<number | null>(null);
// Получаем текущие значения из хранилища
const initialColumns = getSetting('table-columns-order').currentValue as string[];
const disabledColumns = getSetting('table-columns-disabled').currentValue as number[];
const items = pendingChanges['table-columns-order'] ?? initialColumns;
const disabledItems = pendingChanges['table-columns-disabled'] ?? disabledColumns;
// Drag & Drop
const handleDragStart = (index: number) => {
setDragIndex(index);
};
const handleDragOver = (index: number) => {
if (dragIndex === null || index === dragIndex) return;
const newItems = [...items];
const [movedItem] = newItems.splice(dragIndex, 1);
newItems.splice(index, 0, movedItem);
updateSetting('table-columns-order', newItems);
setDragIndex(index);
};
const handleDrop = () => {
setDragIndex(null);
};
// Переключение состояния
const handleSwitch = (index: number) => {
const newDisabledItems = disabledItems.includes(index)
? disabledItems.filter((i: number) => i !== index)
: [...disabledItems, index];
updateSetting('table-columns-disabled', newDisabledItems);
};
// Закрытие с сохранением
const handleSave = () => {
openModal(ModalMap['10']);
};
useEffect(() => {
initializeSettings();
}, [initializeSettings]);
const getItemText = (item: string) => {
if (item === 'Онлайн') return t('settings-status');
if (item === 'Имя') return t('settings-name');
if (item === 'Производитель') return t('settings-vendor');
if (item === 'IP адрес') return t('settings-ipAddress');
if (item === 'Открытые порты') return t('settings-openPorts');
if (item === 'Последняя активность') return t('settings-lastActivity');
if (item === 'Заметка') return t('settings-note');
if (item === 'Действие') return t('settings-action');
return item;
};
return (
<ModalContainer style={{ height: '35vh', width: '80vw'}}>
<ModalTitle>
<h3>{t('customizeTable-title')}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', height: "100%", gap: '24px', padding: 0 }}>
<StyledTableList>
{items.map((item:string, index:number) => (
<StyledTableItem
key={index}
className={disabledItems.includes(index) ? 'off' : ''}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => {
e.preventDefault();
handleDragOver(index);
}}
onDragEnd={handleDrop}
onClick={() => handleSwitch(index)}
>
{getItemText(item)}
</StyledTableItem>
))}
</StyledTableList>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title={t('common-back')}
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};
export default CustomizeNodes;

View file

@ -1,67 +0,0 @@
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
import { useModalContext } from "../../../store"
import { useIpStore } from "../../../store"
import { MainButton } from "../../UI/buttons/mainButton"
import {useState} from "react";
import {NetworkService} from "../../../api/request";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useTranslation} from "../../../utils/i18n/hooks";
type Props={
address: string,
onClose: () => void
}
export const DisconnectConfirm = ({address, onClose}: Props)=>{
const { ipAddress } = useIpStore();
const theme = useTheme()
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation('common');
const handleDisconnect = async () => {
try {
setIsLoading(true);
const res = await NetworkService.disconnectNetwork(ipAddress, address);
if(res.status === 200) {
onClose()
}
} catch (error) {
console.error('Error connection:', error);
}finally {
setIsLoading(false);
}
};
return(
<ModalContainer>
<ModalTitle>
<h3>{t('wifiDisconnect-title')}</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>{t('wifiDisconnect-confirm')}</h3>
<p>{t('wifiDisconnect-message1')} + {address}. {t('wifiDisconnect-message2')}</p>
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton
title={t('cancel')}
onClick={onClose}
size="small"
/>
<MainButton
title={t('wifiDisconnect-disconnect')}
color={theme.errorColor}
hoverColor={theme.errorColor}
onClick={handleDisconnect}
size="small"
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText={t('wifiDisconnect-progress')}/>}
</ModalContainer>
)
}

View file

@ -1,91 +0,0 @@
import { useModalContext } from "../../store";
import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalContainers";
import { ModalMap } from "./windowsType";
import Loader from "../UI/loader";
import { ModalWindow } from "./style";
import {NoteDevice} from "./modalContainers/NoteDevice";
import { NoteWifi } from "./modalContainers/NoteWifi";
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
import {Settings} from "../settings/Settings";
import CustomizeNodes from "./modalContainers/customizeNodes";
type Props = {
isOpen: boolean,
type: string
data: any;
}
export default function WindowComponent({ isOpen, type, data }: Props) {
const { closeModal, closeLoader } = useModalContext()
return (
<>
{isOpen &&
<ModalWindow>
{type == ModalMap['1'] && data &&
<IpConfirm
ipData={data}
onClose={closeModal}
smoothClose={closeLoader}
/>
}
{type == ModalMap['2'] &&
<Loader loaderText={data} />
}
{type === ModalMap['3'] &&
<ConnectionError
message={data.message}
/>
}
{type === ModalMap['6'] && (
<NoteDevice
type={'devices'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['7'] && (
<NoteWifi
type={'wifi'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['8'] && (
<ConnectNetwork
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['9'] && (
<DisconnectConfirm
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['4'] && (
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
)}
{type === ModalMap['5'] && (
<RightsConfirm onClose={closeLoader} />
)}
{type === ModalMap['10'] && (
<Settings
onClose={closeLoader}
/>
)}
{type === ModalMap['11'] && (
<CustomizeNodes />
)}
</ModalWindow >
}
</>
)
}

View file

@ -1,226 +0,0 @@
import {Setting} from "../../store/settings/settingsStore";
import {useState} from "react";
import styled, {useTheme} from "styled-components";
import {IconComponent} from "../../const/buttons/icons";
import {buttonNames} from "../../const/buttons/names";
import SettingSelect from "./SettingSelect";
import {MainButton} from "../UI/buttons/mainButton";
import React from 'react';
import {useModalContext} from "../../store";
import {ModalMap} from "../modalwindows/windowsType";
import {useTranslation} from "../../utils/i18n/hooks";
const SettingBlock = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
margin-bottom: 18px;
font-size: 18px;
width: 90%;
input{
font-size: 16px;
outline: none;
background-color: ${props => props.theme.primary};
color: ${props => props.theme.textColor};
border: 1px solid ${props => props.theme.borderColor};
padding: 6px 8px;
border-radius: 4px;
}
input[type="number"]{
width: 80px;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
`
const StyledCheckbox = styled.input.attrs({ type: 'checkbox' })`
width: 22px !important;
height: 22px !important;
appearance: none;
-webkit-appearance: none;
position: relative;
cursor: pointer;
overflow: hidden;
&::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: ${props => props.theme.primary};
top: 0;
left: 0;
}
&:checked::after {
background: ${props => props.theme.accent};
}
`;
const LabelCheckbox = styled.label`
position: relative;
cursor: pointer;
padding: 0;
svg{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
`
const InputWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 6px;
`
const ErrorText = styled.div`
color: ${props => props.theme.errorColor};
font-size: 12px;
`;
interface SettingItemProps {
setting: Setting;
value: any;
onChange: (value: any) => void;
onValidationError?: (error: string | null) => void;
}
const SettingItem = React.memo(({ setting, value, onChange, onValidationError }: SettingItemProps) => {
const theme = useTheme();
const { t } = useTranslation('common');
const [error, setError] = useState<string | null>(null);
const { openModal } = useModalContext();
const handleButtonAction = () => {
if (setting.id === 'customize-nodes') {
openModal(ModalMap['11']);
} else if (setting.action) {
// Создаем обертку для действия с переводами
const actionWithTranslations = async () => {
try {
// Временно заменяем alert на версию с переводами
const originalAlert = window.alert;
window.alert = (message: string) => {
// Применяем переводы к сообщениям
let translatedMessage = message;
if (message.startsWith('settings-dataNotDeleted')) {
translatedMessage = t('settings-dataNotDeleted') + message.replace('settings-dataNotDeleted', '');
} else if (message === 'settings-exportSuccess') {
translatedMessage = t('settings-exportSuccess');
} else if (message.startsWith('settings-exportError')) {
translatedMessage = t('settings-exportError') + message.replace('settings-exportError', '');
}
originalAlert(translatedMessage);
};
if (setting.action) {
await setting.action();
}
// Восстанавливаем оригинальный alert
window.alert = originalAlert;
} catch (error) {
console.error('Action error:', error);
}
};
actionWithTranslations();
}
};
// Изменение числа
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
let newError: string | null = null;
if (inputValue.trim() === '') {
newError = t('settings-fieldRequired');
onChange(null);
}
else if (isNaN(Number(inputValue))) {
newError = t('settings-enterNumber');
}
else {
const numValue = Number(inputValue);
if (setting.min !== undefined && numValue < setting.min) {
newError = t('settings-valueTooSmall') + setting.min;
}
else if (setting.max !== undefined && numValue > setting.max) {
newError = t('settings-valueTooLarge') + setting.max;
}
if (!newError) {
onChange(numValue);
}
}
setError(newError);
onValidationError?.(newError);
};
return (
<SettingBlock>
<label>{setting.label}</label>
{setting.type === 'boolean' && (
<LabelCheckbox>
<StyledCheckbox
checked={!!value}
onChange={(e) => onChange(e.target.checked)}
/>
<IconComponent name={buttonNames[19]} width="18" height="18" setColor={theme.primary} />
</LabelCheckbox>
)}
{setting.type === 'select' && setting.options && (
<SettingSelect
value={value}
options={setting.options}
onChange={(value) => onChange(value)}
/>
)}
{setting.type === 'text' && (
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)}
{setting.type === 'number' && (
<InputWrapper>
<input
type="number"
min={setting?.min}
max={setting?.max}
value={value === undefined || value === null ? '' : value}
onChange={handleNumberChange}
onKeyDown={(e) => {
if (!/[0-9]/.test(e.key) &&
!['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
e.preventDefault();
}
}}
className={error ? 'error' : ''}
/>
{error && <ErrorText>{error}</ErrorText>}
</InputWrapper>
)}
{setting.type === 'button' && (
<MainButton
type="button"
size="small"
title={setting.defaultValue || ''}
onClick={handleButtonAction}
/>
)}
{setting.type === 'hidden' && null}
</SettingBlock>
);
});
export { SettingItem };

View file

@ -1,118 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { IconComponent } from '../../const/buttons/icons';
import {buttonNames} from "../../const/buttons/names";
interface SettingSelectProps {
value: any;
options: { value: any; label: string }[];
onChange: (value: any) => void;
disabled?: boolean;
}
const SelectWrapper = styled.div`
position: relative;
width: 100%;
max-width: 180px;
`;
const SelectHeader = styled.div<{ $isOpen: boolean; $disabled?: boolean }>`
padding: 6px 8px;
border: 1px solid ${props => props.theme.borderColor};
border-radius: 4px;
background-color: ${props => props.theme.primary};
cursor: ${props => props.$disabled ? 'not-allowed' : 'pointer'};
display: flex;
justify-content: space-between;
align-items: center;
opacity: ${props => props.$disabled ? 0.6 : 1};
font-size: 16px;
`;
const SelectOptions = styled.div<{ $isOpen: boolean }>`
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
background: ${props => props.theme.primary};
border: 1px solid ${props => props.theme.borderColor || '#ccc'};
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
z-index: 10;
display: ${props => props.$isOpen ? 'block' : 'none'};
`;
const OptionItem = styled.div<{ $isSelected: boolean }>`
padding: 6px 8px;
cursor: pointer;
background-color: ${props => props.$isSelected ? (props.theme.accent) : 'transparent'};
color: ${props => props.$isSelected ? (props.theme.primary) : 'inherit'};
`;
const ArrowIcon = styled.span<{ $isOpen: boolean }>`
transition: transform 0.2s;
transform: rotate(${props => props.$isOpen ? '90deg' : '0deg'});
display: flex;
align-items: center;
`;
const SettingSelect: React.FC<SettingSelectProps> = ({value, options, onChange, disabled = false
}) => {
const [isOpen, setIsOpen] = useState(false);
const selectRef = useRef<HTMLDivElement>(null);
const selectedOption = options.find(opt => opt.value === value);
const toggleDropdown = () => {
if (!disabled) {
setIsOpen(!isOpen);
}
};
const handleSelect = (optionValue: any) => {
onChange(optionValue);
setIsOpen(false);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<SelectWrapper ref={selectRef}>
<SelectHeader
$isOpen={isOpen}
onClick={toggleDropdown}
$disabled={disabled}
>
<span>{selectedOption?.label || 'Select...'}</span>
<ArrowIcon $isOpen={isOpen}>
<IconComponent name={buttonNames[6]} width="16" height="16" />
</ArrowIcon>
</SelectHeader>
<SelectOptions $isOpen={isOpen}>
{options.map((option) => (
<OptionItem
key={option.value}
$isSelected={value === option.value}
onClick={() => handleSelect(option.value)}
>
{option.label}
</OptionItem>
))}
</SelectOptions>
</SelectWrapper>
);
};
export default SettingSelect;

View file

@ -1,264 +0,0 @@
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../modalwindows/style";
import { MainButton } from "../UI/buttons/mainButton";
import {useCallback, useEffect, useMemo, useState} from "react";
import styled, {useTheme} from "styled-components";
import {Setting, useSettingsStore} from "../../store/settings/settingsStore";
import {SettingItem} from "./SettingItem";
import {IconComponent} from "../../const/buttons/icons";
import {buttonNames} from "../../const/buttons/names";
import {useTranslation} from "../../utils/i18n/hooks";
interface Props {
onClose: () => void;
}
const SectionsSidebar = styled.ul`
display: flex;
justify-content: start;
align-items: start;
flex-direction: column;
gap: 4px;
width: 20%;
min-width: 180px;
list-style-type: none;
padding: 0;
margin: 0;
`
const SectionsBTN = styled.li`
font-size: 18px;
border: none;
cursor: pointer;
width: 100%;
padding:10px 16px;
box-sizing: border-box;
border-radius: 0 12px 12px 0;
&.active{
background-color: ${(props) => props.theme.accent};
color: ${props => props.theme.primary};
}
`
const SectionActive = styled.div`
width: 80%;
`
const SectionLine = styled.span`
height: 100%;
width: 1px;
background-color: #9D9D9D;
`
const CloseBtn = styled.span`
cursor: pointer;
`
const SettingsButtons = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
span{
display: flex;
gap: 12px;
}
`
export const Settings = ({ onClose }: Props) => {
const settingsConfig = useSettingsStore(state => state.settingsConfig);
const settingsValues = useSettingsStore(state => state.settingsValues);
const activeSection = useSettingsStore(state => state.activeSection);
const pendingChanges = useSettingsStore(state => state.pendingChanges);
const setActiveSection = useSettingsStore(state => state.setActiveSection);
const updateSetting = useSettingsStore(state => state.updateSetting);
const applyChanges = useSettingsStore(state => state.applyChanges);
const discardChanges = useSettingsStore(state => state.discardChanges);
const getSetting = useSettingsStore(state => state.getSetting);
const resetToDefaults = useSettingsStore(state => state.resetToDefaults);
const initializeSettings = useSettingsStore(state => state.initializeSettings);
const updateSettingsConfig = useSettingsStore(state => state.updateSettingsConfig);
const theme = useTheme()
const { t, language } = useTranslation('common');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [currentLanguage, setCurrentLanguage] = useState(language);
// Проверка действий
const handleValidationError = useCallback((fieldId: string, error: string | null) => {
setFieldErrors(prev => {
const newErrors = { ...prev };
if (error) {
newErrors[fieldId] = error;
} else {
delete newErrors[fieldId];
}
return newErrors;
});
}, []);
const settingsBySection = useMemo(() => {
return settingsConfig.reduce((acc, setting) => {
// Применяем переводы к настройкам
const translatedSetting = {
...setting,
sectionId: t(setting.sectionId),
label: t(setting.label),
defaultValue: typeof setting.defaultValue === 'string' && setting.defaultValue.startsWith('settings-') ? t(setting.defaultValue) : setting.defaultValue,
options: setting.options ? setting.options.map((option: any) => ({
...option,
label: option.label.startsWith('settings-') ? t(option.label) : option.label
})) : setting.options,
value: settingsValues[setting.id] ?? setting.defaultValue
};
if (!acc[translatedSetting.sectionId]) {
acc[translatedSetting.sectionId] = [];
}
acc[translatedSetting.sectionId].push(translatedSetting);
return acc;
}, {} as Record<string, Setting[]>);
}, [settingsConfig, settingsValues, t]);
const sections = useMemo(() => Object.keys(settingsBySection), [settingsBySection]);
useEffect(() => {
updateSettingsConfig();
initializeSettings();
}, [updateSettingsConfig, initializeSettings]);
useEffect(() => {
if (sections.length > 0 && !activeSection) {
setActiveSection(sections[0]);
}
}, [sections, activeSection, setActiveSection]);
// Обновляем текущий язык при изменении
useEffect(() => {
setCurrentLanguage(language);
}, [language]);
const handleSave = useCallback(() => {
const hasErrors = Object.values(fieldErrors).some(error => error?.trim());
if (hasErrors) {
alert(t('settings-fixErrors'));
return;
}
const requiredFields = settingsConfig.filter(s => s.required);
const emptyRequiredFields = requiredFields.filter(f => {
const value = pendingChanges[f.id] ?? settingsValues[f.id] ?? f.defaultValue;
return value == null || value === '';
});
if (emptyRequiredFields.length > 0) {
alert(t('settings-fillRequired'));
return;
}
// Проверяем, изменился ли язык
const newLanguage = pendingChanges['language'] ?? settingsValues['language'];
const languageChanged = newLanguage && newLanguage !== currentLanguage;
applyChanges();
onClose();
// Если язык изменился, перезагружаем приложение
if (languageChanged) {
setTimeout(() => {
window.location.reload();
}, 100);
}
}, [fieldErrors, settingsConfig, pendingChanges, settingsValues, applyChanges, onClose, t, currentLanguage]);
const handleCancel = useCallback(() => {
discardChanges();
setFieldErrors({});
onClose();
}, [discardChanges, onClose]);
const handleReset = () => {
if (window.confirm(t('settings-resetConfirm'))) {
resetToDefaults();
}
};
// Обработка нажатий клавиш
useEffect(() => {
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Enter')
handleSave();
if (e.key === 'Escape')
handleCancel()
};
document.addEventListener('keydown', handleKeyPress);
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [handleSave, handleCancel]);
return (
<ModalContainer style={{ height: '60vh', width: '50vw', minWidth: "800px" }}>
<ModalTitle>
<h3>{t('settings-title')}</h3>
<CloseBtn>
<IconComponent name={buttonNames[12]} width="30" height="30" onClick={handleCancel} />
</CloseBtn>
</ModalTitle>
<ModalContent style={{ display: 'flex', height:"100%", gap: '24px', padding:0 }}>
<SectionsSidebar>
{sections.map((sectionId) => (
<SectionsBTN
key={sectionId}
className={activeSection === sectionId ? 'active' : ''}
onClick={()=>setActiveSection(sectionId)}
>
{sectionId}
</SectionsBTN>
))}
</SectionsSidebar>
<SectionLine/>
<SectionActive>
{activeSection && settingsBySection[activeSection] && (
<>
{settingsBySection[activeSection].map((setting) => (
<SettingItem
key={setting.id}
setting={setting}
value={getSetting(setting.id).currentValue}
onValidationError={(error) => handleValidationError(setting.id, error)}
onChange={(value) => updateSetting(setting.id, value)}
/>
))}
</>
)}
</SectionActive>
</ModalContent>
<ButtonContainer>
<SettingsButtons>
<MainButton
type="button"
size="small"
title={t('settings-resetSettings')}
onClick={handleReset}
color={theme.offlineColor}
hoverColor={theme.borderColor}
/>
<span>
<MainButton
type="button"
size="small"
title={t('settings-cancel')}
onClick={handleCancel}
color={theme.offlineColor}
hoverColor={theme.borderColor}
/>
<MainButton
type="button"
size="small"
title={t('settings-save')}
onClick={handleSave}
/>
</span>
</SettingsButtons>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -1,252 +0,0 @@
import { MainButton } from "../../../UI/buttons/mainButton"
import { workSpaceNames } from "../../../../const/workspace/names"
import { useWorkSpaceStore } from "../../../../store"
import styled, {useTheme} from "styled-components"
import ThemeButton from "../../../UI/buttons/toggletheme"
import {buttonNames} from "../../../../const/buttons/names";
import {IconComponent} from "../../../../const/buttons/icons";
import {useTranslation} from "../../../../utils/i18n/hooks";
type Props = {
from: 'reg' | 'helloWindow' | 'workSpace'
onBack?: () => void
}
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
background: ${(props => props.theme.background)};
padding: 50px;
box-sizing: border-box;
`
const Content = styled.div`
max-width: 1200px;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
box-sizing: border-box;
padding: 40px 50px;
background: ${(props => props.theme.primary)};
border-radius: 18px;
position: relative;
`
const Listing = styled.div`
display: flex;
flex-direction: column;
background-color: ${(props => props.theme.secondary)};
border-radius: 10px;
padding: 24px 20px;
p{
font-weight: bold;
font-size: 24px;
}
`
const List = styled.ul`
display: flex;
flex-direction: column;
margin: 10px 0;
padding: 0;
list-style-type: none;
font-size: 18px;
gap: 4px;
& li:before {
color: ${(props => props.theme.accent)};
content: "•";
font-size: 28px;
width: .8em;
display: inline-block;
position: relative;
top: 2px;
}
`
const Title = styled.div`
font-size: 20px;
`
const SubTitle = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
background-color:rgba(255, 248, 225, 0.6);
border-left: #ff9800 solid 4px;
border-radius: 0 8px 8px 0;
padding: 20px 12px;
a{
text-decoration: none;
color: ${(props => props.theme.errorColor)};
font-weight: 500;
font-size: 18px;
width: max-content;
}
p{
font-size: 18px;
color: ${(props=>props.theme.textColor)};
}
.icon{
width: 32px;
height: 32px;
fill: #ff9800;
}
`
const Links = styled.div`
display: flex;
justify-content: start;
gap: 20px;
`
const LinkBlock = styled.a`
text-decoration: none;
color: ${(props => props.theme.accent)};;
`
const Copyright = styled.div`
width: 100%;
text-align: center;
color: ${(props => props.theme.offlineColor)};
`
const ButtonContainer = styled.div`
position: absolute;
top: 12px;
right: 14px;
`
const AboutLine = styled.hr`
border: ${(props => props.theme.hoverBtn)} 1px solid;
width: 100%;
`
const MainTitle = styled.div`
width: 100%;
display: flex;
gap: 15px;
align-items: center;
justify-content: space-between;
padding-top: 10px;
box-sizing: border-box;
img{
width: 50px;
height: 50px;
}
.version{
text-wrap: nowrap;
background-color: ${(props => props.theme.hoverBtn)};
color: ${(props => props.theme.borderColor)};
border-radius: 20px;
font-size: 18px;
padding: 10px 18px;
line-height: 1;
}
.title{
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
}
`
export default function About({ from, onBack }: Props) {
const { setWorkModule } = useWorkSpaceStore()
const {t} = useTranslation('common')
const getBackHandler = () => {
switch (from) {
case 'reg':
return onBack
case 'helloWindow':
return () => setWorkModule(workSpaceNames[1])
case 'workSpace':
return () => setWorkModule(workSpaceNames[3])
default:
return undefined
}
}
const backHandler = getBackHandler()
const showBackButton = from !== 'reg' || (from === 'reg' && onBack)
return (
<Wrapper>
<Content>
<MainTitle>
<div className={'title'}>
<img src="./assets/images/gossys.png" alt={t('about-logoAlt')} />
<div style={{width: '100%', display: "flex", justifyContent: 'center'}}>
<h1>{t('about-title')}</h1>
</div>
</div>
<p className={'version'}>v 0.0.3</p>
</MainTitle>
<AboutLine/>
<Title>
<p>{t('about-description')}</p>
</Title>
<Listing>
<p>{t('about-featuresTitle')}:</p>
<List>
<li>{t('about-feature1')}</li>
<li>{t('about-feature2')}</li>
<li>{t('about-feature3')}</li>
</List>
</Listing>
<SubTitle>
<svg className="icon" viewBox="0 0 24 24">
<path d="M12 2L1 21h22M12 6l7.53 13H4.47M11 10v4h2v-4m-2 6v2h2v-2"/>
</svg>
<p>{t('about-disclaimer')}</p>
<a href={'https://gossys.dev/pages/projects/gonets'}>
{t('about-goToPro')}
</a>
</SubTitle>
<Links>
<LinkBlock href={'https://gossys.dev/pages/projects/gonets'}>
{t('about-homepage')}
</LinkBlock>
<LinkBlock href={'https://wiki.gossys.dev/ru/gonets'}>
{t('about-documentation')}
</LinkBlock>
<LinkBlock href={'https://wiki.gossys.dev/ru/lic/GosysPUB'}>
{t('about-license')}
</LinkBlock>
</Links>
<Copyright>
{t('about-copyright')}
</Copyright>
<ButtonContainer>
{showBackButton && backHandler && (
<IconComponent
width="36"
height="36"
name={buttonNames[12]}
onClick={backHandler}
/>
)}
{from == 'reg' &&
<ThemeButton />
}
</ButtonContainer>
</Content>
</Wrapper>
)
}

View file

@ -1,86 +0,0 @@
import styled from "styled-components"
import { MainButton } from "../../../UI/buttons/mainButton"
import { useWorkSpaceStore } from "../../../../store"
import { workSpaceNames } from "../../../../const/workspace/names"
import { buttonNames } from "../../../../const/buttons/names"
import {useTranslation} from "../../../../utils/i18n/hooks";
const Wrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: ${(props => props.theme.background)};
background-image: url('./assets/images/back1.svg');
background-size: 100%;
background-repeat: no-repeat;
background-position: bottom;
`
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 52px;
margin-bottom: 100px;
`
const ButtonWrapper = styled.div`
display: flex;
gap: 75px;
flex-direction: column;
`
const ButtonContainer = styled.div`
display: flex;
gap: 15px;
flex-direction: column;
`
const Title = styled.div`
display: flex;
align-items: flex-end;
gap: 10px;
h1{
font-size: 52px;
}
`
export const HelloWindow = () => {
const {t} = useTranslation('common')
const { setWorkModule } = useWorkSpaceStore()
return (
<Wrapper>
<Container className="unselectable">
<Title>
<img
src="./assets/images/logo.svg"
alt={t('main-logoAlt')}
style={{width: '152px'}}
/>
</Title>
<ButtonWrapper>
<ButtonContainer>
<MainButton
title={t('main-networkSettings')}
icon={buttonNames[3]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[3])}
/>
<MainButton
title={t('main-about')}
icon={buttonNames[4]}
iconSize={{ width: '32', height: '32' }}
onClick={() => setWorkModule(workSpaceNames[4])}
/>
</ButtonContainer>
</ButtonWrapper>
</Container>
</Wrapper>
)
}

View file

@ -1,85 +0,0 @@
import React, {useState} from 'react';
import styled, {keyframes} from 'styled-components';
import {useTranslation} from "../../../../../../utils/i18n/hooks";
interface Props {
value: string;
setIpScanning: (value: string) => void;
loading: boolean;
isValid: boolean;
setIsValid: (value: boolean) => void;
}
const Input = styled.input`
height: 40px;
width: 210px;
font-size: 20px;
text-align: center;
background-color: transparent;
border: none;
outline: none;
border-bottom: 2px solid ${props => props.theme.offlineColor};
color: ${props => props.theme.textColor};
transition: all 0.3s ease;
&:focus {
border-bottom: 2px solid ${props => props.theme.accent};
width: 270px;
}
&::placeholder {
color: ${props => props.theme.offlineColor};
transition: all 0.3s ease;
}
&:focus::placeholder {
opacity: 0;
}
&:disabled {
opacity: 0;
}
`
const errorVisible = keyframes`
0%{opacity: 0}
100%{opacity: 1}
`;
const ErrorMessage = styled.span`
margin-top: 6px;
color: ${props=>props.theme.errorColor};
animation: ${errorVisible} .4s linear;
`
const IpScanning = ({value, setIpScanning, loading, isValid, setIsValid}: Props) => {
const {t} = useTranslation('common')
const validateInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
const allowedKeys = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'.', '/',
'Backspace', 'Delete',
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'
];
if (!allowedKeys.includes(e.key)) {
e.preventDefault();
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIpScanning(e.target.value);
}
return (
<>
<Input
value={value}
onChange={handleChange}
placeholder={t('ipInput-placeholder')}
onKeyDown={validateInput}
disabled={loading}
/>
{!isValid && !loading && (
<ErrorMessage>{t('ipInput-error')}</ErrorMessage>
)}
</>
);
};
export default IpScanning;

View file

@ -1,115 +0,0 @@
import React, {useEffect, useState} from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
import {usePaginateSetting} from "../../../../../../hooks/useSettings";
import Pagination from "../../../../../UI/Pagination";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
const activeDevices = devices.filter(device => device.status === 'online');
const inactiveDevices = devices.filter(device => device.status !== 'online');
// Пагинация
const paginateSetting = usePaginateSetting();
const [currentPage, setCurrentPage] = useState(1);
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
useEffect(() => {
setDevicesPerPage(paginateSetting);
}, [paginateSetting]);
const indexOfLastDevice = currentPage * devicesPerPage;
const indexOfFirstDevice = indexOfLastDevice - devicesPerPage;
const currentActiveDevices = activeDevices.slice(indexOfFirstDevice, indexOfLastDevice);
const currentInactiveDevices = inactiveDevices.slice(indexOfFirstDevice, indexOfLastDevice);
const totalPages = Math.ceil(devices.length / devicesPerPage);
const handlePrevPage = () => {
if(currentPage > 1) setCurrentPage(currentPage - 1);
}
const handleNextPage = () => {
if(currentPage < totalPages) setCurrentPage(currentPage + 1);
}
const onPageChange = (page: number) => {
setCurrentPage(page);
}
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
</ScanBtn>
{devices.length > 0 ? (
<div style={{
display: 'flex',
width: '70%',
gap: '20px',
}}>
{/* Колонка активных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>
{t('listUsers-activeNodes')} ({activeDevices.length})
</h3>
{currentActiveDevices.map((device, index) => (
<DeviceCard
key={index}
status="online"
device={device}
/>
))}
</div>
{/* Колонка неактивных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>
{t('listUsers-inactiveNodes')} ({inactiveDevices.length})
</h3>
{currentInactiveDevices.map((device, index) => (
<DeviceCard
key={index}
status="offline"
device={device}
/>
))}
</div>
</div>
) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null}
{totalPages > 1 && (
<Pagination
onPageChange={onPageChange}
currentPage={currentPage}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
)}
</>
);
};
export default ListUsers;

View file

@ -1,268 +0,0 @@
import React, {useEffect, useMemo, useState} from 'react';
import { DeviceUser } from '../../../../../../store/useDeviceStore';
import styled from 'styled-components';
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useModalContext} from "../../../../../../store";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
import Pagination from "../../../../../UI/Pagination";
import {
useDisabledColumns,
useNameSetting,
useOrdersColumns,
usePaginateSetting
} from "../../../../../../hooks/useSettings";
interface TableScannerProps {
devices: DeviceUser[];
}
const TableContainer = styled.div`
width: 100%;
margin: 24px 0;
`;
const StyledTable = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
background: ${({ theme }) => theme.primary};
`;
const TableHeaderCell = styled.th<{ sortable?: boolean }>`
padding: 16px 24px;
text-align: left;
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.textColor};
background: ${({ theme }) => theme.primary};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const TableRow = styled.tr<{ clickable?: boolean }>`
transition: all 0.2s ease;
&:hover {
background-color: ${({ theme }) => theme.background};
}
`;
const TableCell = styled.td`
padding: 16px 24px;
font-size: 14px;
color: ${({ theme }) => theme.text};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
button{
background-color: transparent;
border: none;
cursor: pointer;
}
`;
const StatusBadge = styled.span<{ status:string }>`
width: 10px;
height: 30px;
background-color: ${({theme, status}) => (status==='online' ? theme.successColor : theme.offlineColor)};
display: inline-block;
border-radius:4px;
`
export const TableScanner = ({ devices }: TableScannerProps) => {
const { openModal } = useModalContext();
const nameSetting = useNameSetting();
const ordersColumns = useOrdersColumns();
const disabledColumns = useDisabledColumns();
const paginateSetting = usePaginateSetting();
const {t} = useTranslation('common');
const [currentPage, setCurrentPage] = useState(1);
const [devicesPerPage, setDevicesPerPage] = useState(paginateSetting);
console.log(ordersColumns);
useEffect(() => {
setDevicesPerPage(paginateSetting);
}, [paginateSetting]);
const openModalNote = (e: React.MouseEvent, device: DeviceUser) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
const defaultOrder = useMemo(() => ([
t('settings-status'),
t('settings-name'),
t('settings-vendor'),
t('settings-ipAddress'),
t('settings-openPorts'),
t('settings-lastActivity'),
t('settings-note'),
t('settings-action'),
]), [t]);
const normalizedOrders = useMemo(
() => (Array.isArray(ordersColumns) && ordersColumns.length ? ordersColumns : defaultOrder),
[ordersColumns, defaultOrder]
);
const normalizedDisabled = useMemo(
() => (Array.isArray(disabledColumns) ? disabledColumns : []),
[disabledColumns]
);
const visibleColumns = useMemo(() => {
const normalizeColumnName = (column: string) => column?.toLowerCase();
const columnToKey = (column: string) => {
const col = normalizeColumnName(column);
switch (col) {
case 'settings-status':
case normalizeColumnName(t('settings-status')):
case 'онлайн':
case 'status':
return 'status';
case 'settings-name':
case normalizeColumnName(t('settings-name')):
case 'имя':
case 'name':
return 'name';
case 'settings-vendor':
case normalizeColumnName(t('settings-vendor')):
case 'производитель':
case 'vendor':
return 'vendor';
case 'settings-ipaddress':
case normalizeColumnName(t('settings-ipAddress')):
case 'ip адрес':
case 'ip address':
case 'ip':
return 'ip';
case 'settings-openports':
case normalizeColumnName(t('settings-openPorts')):
case 'открытые порты':
case 'open ports':
case 'ports':
return 'ports';
case 'settings-lastactivity':
case normalizeColumnName(t('settings-lastActivity')):
case 'последняя активность':
case 'last activity':
case 'activity':
return 'activity';
case 'settings-note':
case normalizeColumnName(t('settings-note')):
case 'заметка':
case 'note':
return 'note';
case 'settings-action':
case normalizeColumnName(t('settings-action')):
case 'действие':
case 'action':
return 'action';
default:
return '';
}
};
return normalizedOrders
.map((column, index) => ({ column, index }))
.filter(({ index }) => !normalizedDisabled.includes(index))
.map(({ column }) => columnToKey(column))
.filter(Boolean);
}, [normalizedOrders, normalizedDisabled, t]);
// Пагинация
const indexOfLastDevice = currentPage * devicesPerPage;
const indexOfFirstDevice = indexOfLastDevice - devicesPerPage;
const currentDevices = devices.slice(indexOfFirstDevice, indexOfLastDevice);
const totalPages = Math.ceil(devices.length / devicesPerPage);
const handlePrevPage = () => {
if (currentPage > 1) setCurrentPage(currentPage - 1);
};
const handleNextPage = () => {
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
};
const onPageChange = (page: number) => {
setCurrentPage(page);
};
// Рендер ячейки таблицы
const renderTableCell = (column: string, item: DeviceUser) => {
switch (column) {
case 'status':
return <TableCell key="status"><StatusBadge status={item.status} /></TableCell>;
case 'name':
return <TableCell key="name">{item.hostname || (nameSetting === 'ip' ? item.ip : 'Неизвестно')}</TableCell>;
case 'vendor':
return <TableCell key="vendor">{item.vendor}</TableCell>;
case 'ip':
return <TableCell key="ip">{item.ip}</TableCell>;
case 'ports':
return <TableCell key="ports">{item.open_ports.join(', ') || '-'}</TableCell>;
case 'activity':
return <TableCell key="activity">{item.last_seen}</TableCell>;
case 'note':
return <TableCell key="note">{item.note || '-'}</TableCell>;
case 'action':
return (
<TableCell key="action">
<button onClick={(e) => openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" />
</button>
</TableCell>
);
default:
return null;
}
};
return (
<TableContainer>
<StyledTable>
<thead>
<tr>
{visibleColumns.map(column => {
switch (column) {
case 'status': return <TableHeaderCell key="status"></TableHeaderCell>;
case 'name': return <TableHeaderCell key="name">{t('settings-name')}</TableHeaderCell>;
case 'vendor': return <TableHeaderCell key="vendor">{t('settings-vendor')}</TableHeaderCell>;
case 'ip': return <TableHeaderCell key="ip">{t('settings-ipAddress')}</TableHeaderCell>;
case 'ports': return <TableHeaderCell key="ports">{t('settings-openPorts')}</TableHeaderCell>;
case 'activity': return <TableHeaderCell key="activity">{t('settings-lastActivity')}</TableHeaderCell>;
case 'note': return <TableHeaderCell key="note">{t('settings-note')}</TableHeaderCell>;
case 'action': return <TableHeaderCell key="action"></TableHeaderCell>;
default: return null;
}
})}
</tr>
</thead>
<tbody>
{currentDevices.map((item, index) => (
<TableRow key={index}>
{visibleColumns.map(column => renderTableCell(column, item))}
</TableRow>
))}
</tbody>
</StyledTable>
{totalPages > 1 && (
<Pagination
onPageChange={onPageChange}
currentPage={currentPage}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
)}
</TableContainer>
);
};

View file

@ -1,46 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import {DeviceUser} from "../../../../../../store/useDeviceStore";
import {TableScanner} from "./TableScanner";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: (replay?: boolean) => void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const {t} = useTranslation('common')
return (
<>
<ScanBtn onClick={() => handleScan()} disabled={loading}>
{loading ? t('userScanner-loading') : devices.length > 0 ? t('userScanner-refresh') : t('userScanner-scan')}
</ScanBtn>
{devices.length > 0 ? (
<TableScanner devices={devices} />
) : scanInitiated && !loading ? (
<p style={{ marginTop: '20px' }}>{t('userScanner-notFound')}</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -1,247 +0,0 @@
import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components";
import {useEffect, useState, useTransition} from "react";
import { NetworkService } from "../../../../../api/request";
import ListUsers from "./modules/ListUsers";
import TableUsers from "./modules/TableUsers";
import DropdownType from "../../../../UI/Dropdown/DropdownType";
import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIntervalBoolean, useIntervalTime} from "../../../../../hooks/useSettings";
import IpScanning from "./modules/IpScanning";
import {useTranslation} from "../../../../../utils/i18n/hooks";
type Option ={
value: number,
label: string,
}
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:${({scanInitiated, loading})=>(scanInitiated && !loading ? 'start' : 'center')};
height: 100%;
width: 100%;
background: ${(props) => props.theme.background};
padding: 80px 50px 40px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
position: relative;
`;
const TopWrapper = styled.div`
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`
const TopTitle = styled.h2`
font-size: 32px;
width: max-content;
text-align: center;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: ${({scanInitiated,loading})=>(scanInitiated && !loading ? 'scale(0)' : 'scale(1)')};
span {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
export default function UserScanner() {
const {t} = useTranslation('common')
const { ipAddress } = useIpStore();
const [ipScanning, setIpScanning] = useState('');
const [loading, setLoading] = useState(false);
const [isValid, setIsValid] = useState(true);
const { devices, setDevices } = useNetworkStore();
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
const validateIp = (ip: string) => {
const regex = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
return regex.test(ip);
};
// Сканирование
const handleScan = async (replay: boolean) => {
try {
if (ipScanning && !validateIp(ipScanning)) {
setIsValid(false);
return;
}
setIsValid(true);
if (!replay) {
setLoading(true);
}
setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress, ipScanning);
console.log(res)
setDevices(res.data.devices || []);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setDevices([]);
} finally {
setLoading(false);
}
};
// Интервально сканирование
const intervalToggle = useIntervalBoolean();
const intervalTime = useIntervalTime();
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
if (intervalToggle) {
intervalId = setInterval(()=>handleScan(true), intervalTime * 60 * 1000);
}
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [intervalToggle, intervalTime, handleScan]);
// Способ отображения
const viewOptions: Option[] = [
{ value: 1, label: t('userScanner-visible_table') },
{ value: 2, label: t('userScanner-visible_list') },
];
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
const handleViewChange = (option: Option) => {
setSelectedView(option);
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TopWrapper>
{(!scanInitiated || loading) && (<TopTitle>{t('userScanner-title')}</TopTitle>)}
<IpScanning
value={ipScanning}
setIpScanning={setIpScanning}
loading={loading}
isValid={isValid}
setIsValid={setIsValid}
/>
</TopWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
<span className="ring2"></span>
<span className="ring3"></span>
</UserLocate>
{scanInitiated && !loading && (
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label={t('userScanner-visible')} />
)}
{loading ? (
<LoadingText>
{t('userScanner-loading')} <ScanningText />
</LoadingText>
) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
):(
<ListUsers loading={loading} handleScan={()=>handleScan(false)} devices={devices} scanInitiated={scanInitiated} />
)}
</Wrapper>
);
}

View file

@ -1,209 +0,0 @@
import styled, { keyframes } from "styled-components";
import React, { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIpStore} from "../../../../../store";
import {useTranslation} from "../../../../../utils/i18n/hooks";
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:center ;
height: 100%;
width: 100%;
position: relative;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
text-align: center;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: scale(1);
.circel {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
const ScanBtn = styled.button<{scanInitiated: boolean}>`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: transform .4s ease;
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")};
&:hover{
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")} scale(1.03);
}
`
export function WifiScanner() {
const { ipAddress } = useIpStore();
//const ipAddress = '192.168.1.148'
const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const { t } = useTranslation('common');
const handleScan = async () => {
setWifiNetworks([]);
setCurrentNetwork(null);
setLoading(true);
setScanInitiated(true);
try {
const res = await NetworkService.scanWifi(ipAddress);
const networks = res.data.access_points || [];
setWifiNetworks(networks);
} catch (err) {
console.error(t('wifiScanner-scanError'), err);
setWifiNetworks([]);
} finally {
setLoading(false);
}
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>{t('wifiScanner-title')}</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span>
<span className="ring1 circel"></span>
<span className="ring2 circel"></span>
<span className="ring3 circel"></span>
{wifiNetworks.map((network, index) => (
<WifiDot
key={network.bssid}
network={network}
index={index}
total={wifiNetworks.length}
onSelect={setCurrentNetwork}
/>
))}
{currentNetwork && (
<ModalWifiScaner
onClose={() => setCurrentNetwork(null)}
network={currentNetwork}
/>
)}
</UserLocate>
{loading ? (
<LoadingText>
{t('wifiScanner-scanning')} <ScanningText/>
</LoadingText>
) : (
<ScanBtn
onClick={handleScan}
disabled={loading}
scanInitiated={wifiNetworks.length > 0}
>
{wifiNetworks.length > 0 ? t('wifiScanner-refresh') : t('wifiScanner-scan')}
</ScanBtn>
)}
</Wrapper>
);
}

View file

@ -1,228 +0,0 @@
import React, {useEffect, useState} from 'react';
import styled from 'styled-components';
import {WifiNetworkScanner} from "./WifiDot";
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useIpStore, useModalContext} from "../../../../../../store";
import {Status} from "../../../../../customeNodes/nodes/networkInterface/networkNode/style";
import SignalSVG from "../../../../../UI/signalSvg";
import { useTheme } from 'styled-components';
import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore";
import {NetworkService} from "../../../../../../api/request";
import {useTranslation} from "../../../../../../utils/i18n/hooks";
const Modal = styled.div`
display: flex;
justify-content: center;
flex-direction: column;
background-color: ${(props) => props.theme.secondary};
border-radius: 24px;
padding: 32px 28px;
bottom: -40%;
left: 220px;
position: absolute;
z-index: 20;
min-width: 300px;
&:hover .close-btn{
opacity: 1;
visibility: visible;
}
`
const CloseBtn = styled.div`
position: absolute;
top: -20px;
right: -20px;
cursor: pointer;
background-color: ${(props) => props.theme.primary};
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
transition: all .4s ease;
opacity: 0;
visibility: visible;
&:hover {
background-color: ${(props) => props.theme.hoverBtn};
}
`
const ModalTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
line-height: 1;
margin-bottom: 32px;
h4{
font-weight: normal;
}
.functionalIcons{
display: flex;
justify-content: center;
align-items: center;
gap: 18px;
}
`
const ModalContent = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
justify-content: space-between;
align-items: center;
width: 100%;
`
const ModalRow = styled.div`
display: flex;
justify-content: space-between;
align-items: start;
gap: 12px;
width: 100%;
font-size: 14px;
h6{
margin: 0;
padding: 0;
font-size: 1em;
}
p{
max-width:180px;
text-align: right;
}
`
const ModalConnect = styled.button`
color: ${(props) => props.theme.primary};
padding: 10px 12px;
font-size: 18px;
background-color: ${(props) => props.theme.accent};
border-radius: 12px;
margin-top: 4px;
border: none;
outline: none;
width: 100%;
cursor: pointer;
transition: .4s ease;
&:hover{
background-color: ${(props) => props.theme.accentHover};
}
`
const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onClose: ()=>void }) => {
const { openModal } = useModalContext();
const { ipAddress } = useIpStore();
const theme = useTheme()
const [signal, setSignal] = useState(1);
const { wifiNetworks } = useNetworkStore();
const [isConnected, setIsConnected] = useState(false);
const { t } = useTranslation('common');
useEffect(() => {
const getStatus = async () => {
try {
const response = await NetworkService.statusNetwork(ipAddress);
const { status, ssid } = response.data;
setIsConnected(
!!network?.ssid &&
ssid === network.ssid &&
status === 'connected'
);
} catch (error) {
setIsConnected(false);
}
};
getStatus();
}, [network?.ssid, ipAddress]);
const currentNetwork = wifiNetworks.find(n => n.bssid === network.bssid) || network;
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['7'], {
address: currentNetwork.ssid,
currentNote: currentNetwork.note || ''
});
};
const openModalNetwork= (e: React.MouseEvent) => {
e.stopPropagation();
if(isConnected){
openModal(ModalMap['9'], {
address: currentNetwork.ssid,
});
}else{
openModal(ModalMap['8'], {
address: currentNetwork.ssid,
});
}
};
useEffect(() => {
const perc = Number(network.signal);
switch (true) {
case perc >= -50: setSignal(5); break;
case perc >= -60: setSignal(4); break;
case perc >= -70: setSignal(3); break;
case perc >= -80: setSignal(2); break;
default: setSignal(1); break;
}
}, [network.signal])
return (
<Modal>
<CloseBtn className={'close-btn'} onClick={onClose}>
<IconComponent name={buttonNames[12]} width="30" height="30" />
</CloseBtn>
<ModalTitle>
<h4>{network.ssid}</h4>
<div className={'functionalIcons'}>
<span onClick={openModalNote} style={{cursor: "pointer"}}>
<IconComponent name={buttonNames[16]} width="30" height="30" />
</span>
<Status>
<SignalSVG signal={signal} size={'36px'} color={theme.accent} />
</Status>
</div>
</ModalTitle>
<ModalContent>
<ModalRow>
<h6>{t('wifiModal-bssid')}:</h6>
<p>{network.bssid}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-vendor')}:</h6>
<p>{network.vendor || t('wifiModal-unknown')}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-signal')}:</h6>
<p>{network.signal} dBm</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-channel')}:</h6>
<p>{network.channel}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-encryption')}:</h6>
<p>{network.encryption || t('wifiModal-none')}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-frequency')}:</h6>
<p>{network.frequency} MHz</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-hidden')}:</h6>
<p>{network.hidden ? t('common-yes') : t('common-no')}</p>
</ModalRow>
<ModalRow>
<h6>{t('wifiModal-note')}:</h6>
<p>{currentNetwork.note || '-'}</p>
</ModalRow>
<ModalConnect onClick={openModalNetwork}>
{isConnected ? t('wifiModal-disconnect') : t('wifiModal-connect')}
</ModalConnect>
</ModalContent>
</Modal>
);
};
export default ModalWifiScaner;

View file

@ -1,81 +0,0 @@
import React from 'react';
import styled from "styled-components";
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
//Точка вайфай
const Dot = styled.div<{ top: string; left: string }>`
position: absolute;
top: ${props => props.top};
left: ${props => props.left};
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
transform: translate(-50%, -50%);
user-select: none;
transition: .4s;
text-wrap: nowrap;
&:hover {
transform: translate(-50%, -50%) scale(1.1);
}
`;
const getPositionInRing = (index: number, total: number, ringSize: number) => {
const angle = (index * (2 * Math.PI / total)) + (Math.PI / total * (index % 2));
const innerRadius = ringSize * 0.4;
const outerRadius = ringSize * 0.5;
const radius = innerRadius + (outerRadius - innerRadius) * (0.3 + 0.7 * (index % 10) / 10);
return {
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius
};
};
const WifiDot = ({ network, index, total, onSelect }: {
network: WifiNetworkScanner,
index: number,
total: number,
onSelect: (n: WifiNetworkScanner) => void
}) => {
const getPosition = (signal: number, index: number, total: number) => {
let strength: 1 | 2 | 3;
if (signal >= -50) {
strength = 3;
} else if (signal >= -70) {
strength = 2;
} else {
strength = 1;
}
const ringSizes = { 1: 550, 2: 400, 3: 250 };
const ringSize = ringSizes[strength];
const offset = getPositionInRing(index, total, ringSize);
return {
top: `calc(50% + ${offset.y}px)`,
left: `calc(50% + ${offset.x}px)`
};
};
const position = getPosition(network.signal, index, total);
return (
<Dot top={position.top} left={position.left} onClick={() => onSelect(network)}>
{network.ssid}
</Dot>
);
};
export default WifiDot;

View file

@ -1,9 +0,0 @@
interface Window {
electron: {
quitApp: () => void;
};
electronAPI: {
showSaveDialog: (options: Electron.SaveDialogOptions) => Promise<Electron.SaveDialogReturnValue>;
writeFile: (path: string, data: string) => Promise<{ success: boolean }>;
};
}

View file

@ -1,37 +0,0 @@
import {useSettingsStore} from "../store/settings/settingsStore";
import {defaultLanguage, Language} from "../utils/i18n/config";
export const useNameSetting = () => {
return useSettingsStore(state => state.getSettingValue('netnames-nodes')) as string;
};
export const usePaginateSetting = () => {
return useSettingsStore(state => state.getNumberSetting('paginate-nodes', 20));
};
export const useThemeSettings = () => {
return useSettingsStore(state => state.getSettingValue('theme'));
};
//Интервальное сканирование под узлов
export const useIntervalBoolean = () => {
return useSettingsStore(state => state.getSettingValue('rescan-nodes')) as boolean;
};
export const useIntervalTime = () => {
return useSettingsStore(state => state.getNumberSetting('interval-nodes', 60));
};
// Кастомизация таблиц
export const useOrdersColumns = () => {
return useSettingsStore(state => state.getSettingValue('table-columns-order')) as string[];
};
export const useDisabledColumns = () => {
return useSettingsStore(state => state.getSettingValue('table-columns-disabled')) as number[];
};
export const useLanguage = () => {
return useSettingsStore(
state => state.getSettingValue<Language>('language')
) ?? defaultLanguage;
};

View file

@ -1,30 +0,0 @@
const createAppearanceSettings = () => {
return [
{
id: 'theme',
sectionId: 'settings-sections-appearance',
label: 'settings-colorTheme',
type: 'select',
defaultValue: 'light',
options: [
{ value: 'light', label: 'settings-light' },
{ value: 'dark', label: 'settings-dark' },
{ value: 'system', label: 'settings-system' }
],
},
{
id: 'language',
sectionId: 'settings-sections-appearance',
label: 'settings-interfaceLanguage',
type: 'select',
defaultValue: 'ru',
options: [
{ value: 'ru', label: 'settings-russian' },
{ value: 'en', label: 'settings-english' },
],
},
] as const;
};
export { createAppearanceSettings };

View file

@ -1,55 +0,0 @@
import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNetworkSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{
id: 'clean-network',
type: 'button',
label: 'settings-clearSessionData',
sectionId: 'settings-sections-accessPoints',
defaultValue: 'settings-clear',
action: async ()=>{
try {
const res = await NetworkService.clearNetwork(ipAddress);
console.log(res);
}catch (e){
console.error(e);
alert('settings-dataNotDeleted' + e)
}
},
},
{
id: 'export-network',
type: 'button',
label: 'settings-exportNetworkData',
sectionId: 'settings-sections-accessPoints',
defaultValue: 'settings-export',
action: async () => {
try {
const response = await NetworkService.exportNetwork(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({
title: 'settings-saveNetworksTitle',
defaultPath: "network_export.json",
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
await window.electronAPI.writeFile(
filePath,
JSON.stringify(response.data, null, 2)
);
alert('settings-exportSuccess');
}
} catch (e: any) {
alert('settings-exportError' + e.message);
}
},
},
] as const;
};
export { createNetworkSettings };

View file

@ -1,119 +0,0 @@
import {useIpStore} from "../../ipStore";
import {NetworkService} from "../../../api/request";
const createNodesSettings = () => {
const { ipAddress } = useIpStore.getState();
return [
{
id: 'rescan-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-intervalScanning',
type: 'boolean',
defaultValue: false,
},
{
id: 'interval-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-scanInterval',
type: 'number',
defaultValue: 60,
min: 1,
max: 2880
},
{
id: 'netnames-nodes',
sectionId: 'settings-sections-subnetNodes',
label: 'settings-displayNames',
type: 'select',
defaultValue: 'ip',
options: [
{ value: 'ip', label: 'settings-ipAddress' },
{ value: 'unknown', label: 'settings-unknown' },
],
},
{
id: 'paginate-nodes',
type: 'number',
label: 'settings-rowsPerPage',
sectionId: 'settings-sections-subnetNodes',
defaultValue: 20,
min: 1,
},
{
id: 'customize-nodes',
type: 'button',
label: 'settings-displaySettings',
sectionId: 'settings-sections-subnetNodes',
defaultValue: 'settings-configure',
},
{
id: 'clean-nodes',
type: 'button',
label: 'settings-clearSessionData',
sectionId: 'settings-sections-subnetNodes',
defaultValue: 'settings-clear',
action: async ()=>{
try {
const res = await NetworkService.clearSubnet(ipAddress);
console.log(res)
}catch (e){
console.error(e);
alert('settings-dataNotDeleted' + e)
}
},
},
{
id: "export-nodes",
type: "button",
label: "settings-exportData",
sectionId: "settings-sections-subnetNodes",
defaultValue: "settings-export",
action: async () => {
try {
const response = await NetworkService.exportDevices(ipAddress);
const { filePath } = await window.electronAPI.showSaveDialog({
title: "settings-saveNodesTitle",
defaultPath: "nodes_export.json",
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (filePath) {
await window.electronAPI.writeFile(
filePath,
JSON.stringify(response.data, null, 2)
);
alert("settings-exportSuccess");
}
} catch (e: any) {
alert("settings-exportError" + e.message);
}
},
},
{
id: 'table-columns-order',
type: 'hidden',
label: '',
sectionId: 'settings-sections-subnetNodes',
defaultValue: [
'settings-status',
'settings-name',
'settings-vendor',
'settings-ipAddress',
'settings-openPorts',
'settings-lastActivity',
'settings-note',
'settings-action'
]
},
{
id: 'table-columns-disabled',
label: '',
sectionId: 'settings-sections-subnetNodes',
type: 'hidden',
defaultValue: []
}
] as const;
};
export { createNodesSettings };

View file

@ -1,139 +0,0 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import {createNodesSettings} from "./config/nodesSettings";
import { createNetworkSettings } from './config/networkSettings';
import {createAppearanceSettings} from "./config/appearanceSettings";
type SettingValue = any;
type SettingsDictionary = Record<string, SettingValue>;
export type Setting = {
readonly id: string;
readonly type: 'boolean' | 'select' | 'text' | 'number' | 'button' | 'hidden';
readonly label: string;
readonly defaultValue: any;
value?: string;
readonly min?: number;
readonly max?: number;
readonly options?: any;
readonly sectionId: string;
readonly required?: boolean;
readonly action?: () => void;
};
type SettingsState = {
settingsConfig: Setting[];
settingsValues: SettingsDictionary;
activeSection: string | null;
pendingChanges: SettingsDictionary;
setActiveSection: (sectionId: string | null) => void;
updateSetting: (id: string, value: any) => void;
initializeSettings: () => void;
getSetting: (id: string) => Setting & { currentValue: any };
resetSettings: () => void;
applyChanges: () => void;
discardChanges: () => void;
resetToDefaults: () => void;
getSettingValue: <T = any>(id: string) => T;
getNumberSetting: (id: string, fallback?: number) => number;
updateSettingsConfig: () => void;
};
export const useSettingsStore = create<SettingsState>()(
persist(
(set, get) => ({
settingsConfig: [],
settingsValues: {},
activeSection: null,
pendingChanges: {},
setActiveSection: (sectionId) => set({ activeSection: sectionId }),
updateSetting: (id, value) =>
set((state) => ({
pendingChanges: { ...state.pendingChanges, [id]: value }
})),
resetToDefaults: () => set({
settingsValues: Object.fromEntries(
get().settingsConfig.map(setting => [
setting.id,
setting.defaultValue
])
),
pendingChanges: {}
}),
applyChanges: () =>
set((state) => ({
settingsValues: { ...state.settingsValues, ...state.pendingChanges },
pendingChanges: {}
})),
discardChanges: () => set({ pendingChanges: {} }),
initializeSettings: () => {
const { settingsValues, settingsConfig } = get();
const newValues = { ...settingsValues };
settingsConfig.forEach(setting => {
if (newValues[setting.id] === undefined) {
newValues[setting.id] = setting.defaultValue;
}
});
set({ settingsValues: newValues });
},
updateSettingsConfig: () => {
const defaultSettings: Setting[] = [
...createNodesSettings(),
...createNetworkSettings(),
...createAppearanceSettings(),
];
set({ settingsConfig: defaultSettings });
},
getSetting: (id) => {
const state = get();
const setting = state.settingsConfig.find(s => s.id === id);
if (!setting) throw new Error(`Setting ${id} not found`);
const hasPendingChange = Object.prototype.hasOwnProperty.call(state.pendingChanges, id);
const hasSavedValue = Object.prototype.hasOwnProperty.call(state.settingsValues, id);
const currentValue = hasPendingChange
? state.pendingChanges[id]
: hasSavedValue
? state.settingsValues[id]
: setting.defaultValue;
return {
...setting,
currentValue
};
},
getSettingValue: (id: string) => {
const state = get();
return state.settingsValues[id] ??
state.settingsConfig.find(s => s.id === id)?.defaultValue;
},
getNumberSetting: (id: string, fallback = 0) => {
const value = get().getSettingValue(id);
return Number(value) || fallback;
},
resetSettings: () => set({ settingsValues: {}, pendingChanges: {} }),
}),
{
name: 'app-settings',
partialize: (state) => ({
settingsValues: state.settingsValues
}),
onRehydrateStorage: () => (state) => {
state?.initializeSettings();
}
}
)
);

View file

@ -1,51 +0,0 @@
import { create } from 'zustand';
export interface DeviceUser {
ip: string;
mac: string;
hostname: string;
vendor: string;
open_ports: string[];
status: string;
last_seen: string;
note: string;
}
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
interface NetworkStore {
devices: DeviceUser[];
setDevices: (devices: DeviceUser[]) => void;
addDevice: (device: DeviceUser) => void;
wifiNetworks: WifiNetworkScanner[];
setWifiNetworks: (networks: WifiNetworkScanner[]) => void;
addWifiNetwork: (network: WifiNetworkScanner) => void;
clearAll: () => void;
}
const useNetworkStore = create<NetworkStore>((set) => ({
devices: [],
setDevices: (devices) => set({ devices }),
addDevice: (device) =>
set((state) => ({ devices: [...state.devices, device] })),
wifiNetworks: [],
setWifiNetworks: (networks) => set({ wifiNetworks: networks }),
addWifiNetwork: (network) =>
set((state) => ({ wifiNetworks: [...state.wifiNetworks, network] })),
clearAll: () => set({ devices: [], wifiNetworks: [] })
}));
export default useNetworkStore;

View file

@ -1,11 +0,0 @@
export const supportedLanguages = {
en: 'English',
ru: 'Русский',
} as const;
export type Language = keyof typeof supportedLanguages;
export const defaultLanguage = 'ru';
export const namespaces = ['common', 'components'] as const;
export type Namespace = (typeof namespaces)[number];

View file

@ -1,11 +0,0 @@
import { Namespace } from './config';
import { useI18n } from './provider';
export const useTranslation = (ns?: Namespace) => {
const { t, language } = useI18n();
return {
t: (key: string) => t(key, ns),
language,
};
};

View file

@ -1,92 +0,0 @@
'use client'
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { defaultLanguage, Language, Namespace, namespaces, supportedLanguages } from './config';
type Translation = Record<string, string>;
type NamespaceTranslations = Record<Namespace, Translation>;
type LanguageTranslations = Record<Language, NamespaceTranslations>;
const I18nContext = createContext<{
language: Language;
t: (key: string, namespace?: Namespace) => string;
}>({
language: defaultLanguage,
t: (key: string) => key,
});
interface I18nProviderProps {
children: React.ReactNode;
initialLanguage: Language;
}
export const I18nProvider = ({ children, initialLanguage }: I18nProviderProps) => {
const [language, setLanguage] = useState<Language>(initialLanguage);
const [translations, setTranslations] = useState<LanguageTranslations>({} as LanguageTranslations);
// Загрузка переводов
useEffect(() => {
const loadTranslations = async () => {
const newTranslations: LanguageTranslations = { ...translations };
await Promise.all(
(Object.keys(supportedLanguages) as Language[]).map(async (lang) => {
if (!newTranslations[lang]) {
newTranslations[lang] = {} as NamespaceTranslations;
await Promise.all(
namespaces.map(async (ns) => {
try {
const res = await fetch(`/locales/${lang}/${ns}.json`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
newTranslations[lang][ns] = data;
} catch (e) {
console.error(`Failed to load ${lang}/${ns} translations`, e);
newTranslations[lang][ns] = {};
}
})
);
}
})
);
setTranslations(newTranslations);
};
loadTranslations();
}, []);
useEffect(() => {
setLanguage(initialLanguage);
}, [initialLanguage]);
const t = useMemo(() => {
return (key: string, namespace: Namespace = 'common') => {
return translations[language]?.[namespace]?.[key] || key;
};
}, [language, translations]);
const contextValue = useMemo(() => ({
language,
t,
}), [language, t]);
return (
<I18nContext.Provider value={contextValue}>
{children}
</I18nContext.Provider>
);
};
export const useI18n = () => {
const context = useContext(I18nContext);
if (!context) {
throw new Error('useI18n must be used within I18nProvider');
}
return context;
};

File diff suppressed because it is too large Load diff

View file

@ -1,56 +0,0 @@
## Клиент приложения ГоNets
### Общие сведения
ГоNets - приложение для работы с сетевыми ресурсами в удобном и понятном графическом виде. Данное приложение разделено на серверную и клиентскую часть. Сервер сканирует, обрабатывает и преобразует информацию по сетям в простой и понятный JSON формат, который в свою очередь приходит на клиент, где данные проходят ряд обработок и формируются в графическое отображение.
Клиент приложения ГоNets написан на языке Typescript с использованием фреймворка React. Для более понятного отображения информации была выбрана библиотека ReatFlow, позволяющая легко и просто выстраивать различные графы, схемы, блоки и их связи. Так же, для того, чтобы приложение запускалось как десктопное был выбран метод обёртки собранной (build) версии в облочку Electron.
### Зависимости клиенсткой части
Далее будет представлен список необходимых npm зависимостей для разработки приложения. Уточню, что необходимы они исключительно во время разработки и сборки, собранная версия работает спокойно без них.
Зависимости при разработки:
```
"dependencies": {
"@iconify/icons-ic": "^1.2.13",
"@iconify/react": "^5.2.0",
"@reduxjs/toolkit": "^2.6.0",
"@xyflow/react": "^12.3.5",
"axios": "^1.7.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-flow-renderer": "^10.3.17",
"react-redux": "^9.2.0",
"react-router-dom": "^7.0.2",
"styled-components": "^6.1.13",
"zustand": "^5.0.3"
},
"devDependencies": {
"@iconify-json/ic": "^1.2.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.4.1",
"electron": "^34.0.2",
"electron-packager": "^17.1.2",
"esbuild-wasm": "^0.17.12",
"typescript": "^4.9.5",
"vite": "^6.3.4"
}
```
### Запуск и сборка
Для того, чтобы работать с исходным кодом, необходимо склонировать проект с ветки frontend, перейти в директорию `FRONT/DEV/` и выполнить установку зависимостей с помощью `npm install`, возможно потребуется сделать после этого `npm audit --fix` Весь исходный код, находится непосредственно в директории `DEV`, собранная же версия в директории `BUILD`.
Для сборки приложения и обёртки его в электрон необходимо выполнить команду `npm run package`, далее перейти в созданную после команды директорию `dist`, от туда в `gonets-for-linux-64x/resources/app`, здесь нужно выполнить `npm run build`, в появившейся директории `dist` отредактировать файл `index.html`, таким образом, что везде где стоит `/assets` нужно прописать `./assets`. Проверить запуск приложения, можно прописав в терминале `./gonets` из директории `FRONT/DEV/dist/gonets-for-linux-64x`, где и лежит исполняемвый файл.
После удачного билда можно переместить всё содержимое `FRONT/DEV/dist/gonets-for-linux-64x/` в `/FRONT/BUILD`
Дополнительно отмечу, что входной точкой/файлом настройки самого электрона является файл `main.js`

View file

@ -1,72 +1,3 @@
<p align="center"><img src="assets/gonets_logo.svg" alt="Логотип ГоNets" width="15%"></p> # GoNets
<p align="center"><img src="assets/gonets_text.svg" alt="Написание ГоNets" width="70%"></p>
# ОГЛАВЛЕНИЕ
- [Ссылки](#cссылки:)
- [Что это?](#что-это?)
- [Почему MVP?](#почему-mvp?)
- [Текущий статус Приложения](#текущий-статус-приложения)
- [Стабильные версии:](#стабильные-версии:)
- [Правила наименования](#правила-наименования)
- [Коды компонентов](#коды-компонентов)
- [Нумерация версий](#нумерация-версий)
## Ссылки:
Здесь Вы найдете неполный список ссылок на официальные ресурсы:
- [WEB-страница приложения](https://gonets.gossys.dev)
- [Гибридная Офисная Система](https://hybryd.ru)
- [HELPDESK](https://helpdesk.gossys.dev/)
- [Раздел Базы знаний](https://helpdesk.gossys.dev/knowledgebase.php?category=17)
- [О Разработчике](https://gossys.dev/)
- [Документация](https://wiki.gossys.dev/docs/gonets)
## Что это?
**ГоNets** - это кроссплатформенное **MVP** - приложение для управления сетевыми настройками локального и удаленных Клиентов и проведения востребованных Пользователями и Администраторами сетевых операций.
Все это происходит с использованием нового, интуитивно понятного интерфейса, с полным интерактивным взаимодействием.
<p align="center"><img src="assets/work_scr.jpg" alt="Экран ГоNets" width="90%"></p>
## Почему MVP?
**ГоNets** - приложение, созданное для тестирования данного интерфейса, применительно к управлению настройками операционной системы. На основе Вашей реакции нами будут приняты решения о дальнейшей модернизации проекта, либо его полного перехода на более привычный интерфейс.
## Текущий статус Приложения
Текущий статус Приложения - В разработке. Чего еще ожидать от приложения, размещенного на Гит? Одновременно с этим - оно находится и в тестировании. Подробную информацию Вы можете получить в в разделах Задачи и Проекты.
### Стабильные версии:
- Версия для Deb-based: GNB-DEB64-0.0.3
- Версия для RPM-based: В разработке
- Версия для Windows: В разработке
- Версия для MacOS (Intel): В разработке
- Версия для MacOS (ARM): В разработке
## Правила наименования
Общий шаблон наименования - **CODE-ARCH-NUM-SPEC**, где:
- **CODE** - Код компонента (См. таблицу ниже);
- **ARCH** - Архитектура компонента в соответствии с международной классификацией;
- **NUM** - Номер версии, в соответствии с "Правилами нумерации версий";
- **SPEC** - Дополнительное буквенное обозначение в общепринятом формате. (Например RC, Beta, PreRelease)
### Коды компонентов
|Наименование|Описание|Код|
|---|---|---|
|**Project GoNets (Проект, Проект GoNets, GoNets, Программный комплекс, ПК)**| Программный комплекс вцелом.| ***GNP*** |
|**Server (Сервер)**| Программный компонент, отвечающий за получение и обработку данных, формирующий данные для последующего отображения|***GNS***|
|**Server Client (Серверный Клиент)**| Программный компонент, отвечающий за сбор данных с ПК или Сервера и передающий их для обработки|***GNK***|
|**Client (Клиент)** |Программный компонент, отвечающий за визуализацию и взаимодействие с Пользователем|***GNC***|
|**Bundle (Объединенное приложение)**| Версия приложения **ГоNets**, собранная из нескольких программных компонентов, объединенная одним инсталлятором для обспечения полного функционала|***GNB***|
### Нумерация версий
В проекте действует следующая система нумерации версий:
**XX.YY.ZZ** - где XX.YY.ZZ, обозначают:
- **ZZ**: текущий номер стабильного билда;
- **YY**: текущий номер минорной версии (Версии с полной обратной совместимостью с предыдущими, без кардинальных отличий в интерфейсе или функционале.)
- **XX**: Текущий номер мажорной версии (Версии с частичной или отсутствующей обратной совместимостью с предыдущими, либо с кардинальными отличиями в интерфейсе или функционале.)
Общий репозиторий для GoNets начиная с версии 0.0.1

2
SRV/.gitignore vendored
View file

@ -6,5 +6,3 @@ __pycache__
.wheels .wheels
.vendor .vendor
.python3-deb .python3-deb
storage/subnets/
storage/wifi/

View file

@ -1,29 +1,5 @@
"""
Маршрутизация API-версии v1 для GONETS.
Модуль агрегирует и монтирует роутеры отдельных доменных областей под общим
корневым роутером. Каждый подключенный модуль предоставляет набор эндпоинтов
со своим префиксом и тегами для группировки в документации FastAPI.
Структура маршрутов:
- `/ufw` (UFW): управление правилами и состоянием Uncomplicated Firewall.
- `/ip` (IP): проверки и применение IP-конфигураций для сетевых профилей.
- `/network` (Network): получение и визуализация состояния интерфейсов,
управление позициями UI-контейнеров.
- `/auth` (Auth): регистрация, аутентификация (JWT), сессии.
- `/subnet` (Subnet): сканирование подсетей, заметки к устройствам,
взаимодействие с Wi-Fi (скан/подключение/отключение/статус).
- `WebSocket`: обработчики взаимодействия по WS (без префикса, путь внутри
модуля), периодическая трансляция сетевых данных.
- `/settings` (Settings): сервисные операции над локальными JSON-хранилищами
(очистка данных подсетей/wi-fi и пр.).
Назначение:
- Централизованная точка подключения доменных роутеров к приложению.
- Единообразие тегов и префиксов для автогенерации схемы и документации.
"""
from fastapi import APIRouter from fastapi import APIRouter
from .v1 import ufw, websocket, ip, network, auth, subnet, settings from api.v1 import ufw, websocket, ip, network, auth
router = APIRouter() router = APIRouter()
@ -32,7 +8,3 @@ router.include_router(websocket.router, tags=["WebSocket"])
router.include_router(ip.router, prefix="/ip", tags=["IP"]) router.include_router(ip.router, prefix="/ip", tags=["IP"])
router.include_router(network.router, prefix="/network", tags=["Network"]) router.include_router(network.router, prefix="/network", tags=["Network"])
router.include_router(auth.router, prefix="/auth", tags=["Auth"]) router.include_router(auth.router, prefix="/auth", tags=["Auth"])
router.include_router(subnet.router, prefix="/subnet", tags=["Subnet"])
router.include_router(settings.router, prefix="/settings", tags=["Settings"])

View file

@ -1,10 +1,9 @@
# api/v1/auth.py
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from services.auth_service import AuthService from services.auth_service import AuthService
from models.auth import UserCreate, UserLogin, UserResponse, Token from models.auth import UserCreate, UserLogin, UserResponse, Token
from storage.json_storage import JsonStorage from storage.json_storage import JsonStorage
from utils.i18n import get_translations, _
from babel.support import Translations
import logging import logging
import os import os
@ -15,24 +14,10 @@ router = APIRouter()
auth_service = AuthService() auth_service = AuthService()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
@router.post("/register", response_model=UserResponse) @router.post("/register", response_model=UserResponse)
async def register(user: UserCreate, translations: Translations = Depends(get_translations)): async def register(user: UserCreate):
"""Регистрирует нового пользователя. """Регистрирует нового пользователя"""
Создаёт нового пользователя на основе предоставленных данных и возвращает
информацию о зарегистрированном пользователе.
Args:
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
translations: Объект переводов для текущего языка.
Returns:
UserResponse: Объект с данными зарегистрированного пользователя (id, username, is_admin).
Raises:
HTTPException: Если пользователь уже существует (status_code=400) или произошла
внутренняя ошибка (status_code=500).
"""
try: try:
db_user = auth_service.register(user) db_user = auth_service.register(user)
logger.info(f"Пользователь {user.username} успешно зарегистрирован") logger.info(f"Пользователь {user.username} успешно зарегистрирован")
@ -40,116 +25,62 @@ async def register(user: UserCreate, translations: Translations = Depends(get_tr
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Error in register: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось зарегистрировать пользователя")) raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя")
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)
async def login(user: UserLogin, request: Request, translations: Translations = Depends(get_translations)): async def login(user: UserLogin, request: Request):
"""Авторизует пользователя и создаёт сессию. """Авторизует пользователя и создаёт сессию"""
Проверяет учетные данные пользователя и возвращает токен и 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: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Попытка входа в систему клиента {client_id}") logger.debug(f"Попытка авторизации для client_id {client_id}")
result = auth_service.login(user, client_id) result = auth_service.login(user, client_id)
logger.info(f"Успешная авторизация для клиента {client_id}, Идентификатор сессии: {result['session_id']}") logger.info(f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}")
return result return result
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка при авторизации: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось авторизовать пользователя")) raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя")
@router.get("/me") @router.get("/me")
async def get_current_user(request: Request, translations: Translations = Depends(get_translations)): async def get_current_user(request: Request):
"""Возвращает данные текущего пользователя и токен. """Возвращает данные текущего пользователя и токен"""
Проверяет активную сессию по 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: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Запрос /me для клиента {client_id}") logger.debug(f"Запрос /me для client_id {client_id}")
result = auth_service.get_user_by_session(client_id) result = auth_service.get_user_by_session(client_id)
return { return {
"user": result["user"], "user": result["user"],
"access_token": result["access_token"], "access_token": result["access_token"],
"token_type": translations.gettext("bearer"), "token_type": "bearer"
} }
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка при get_current_user: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить данные пользователя")) raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя")
@router.get("/logout") @router.get("/logout")
async def logout(request: Request, translations: Translations = Depends(get_translations)): async def logout(request: Request):
"""Выход из системы, удаление сессии. """Выход из системы, удаление сессии"""
Удаляет активную сессию пользователя по client_id.
Args:
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
translations: Объект переводов для текущего языка.
Returns:
dict: Пустой словарь или сообщение об успешном выходе.
Raises:
HTTPException: Если сессия не найдена (status_code=401) или произошла
внутренняя ошибка (status_code=500).
"""
try: try:
client_id = request.client.host client_id = request.client.host
logger.debug(f"Запрос на выход из системы для клиента {client_id}") logger.debug(f"Запрос logout для client_id {client_id}")
return auth_service.logout(client_id) return auth_service.logout(client_id)
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Не удалось выйти из системы: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось выйти из системы")) raise HTTPException(status_code=500, detail="Не удалось выйти из системы")
@router.get("/check_json") @router.get("/check_json")
async def check_users_file(translations: Translations = Depends(get_translations)): async def check_users_file():
"""Проверяет, существует ли файл users.json и содержит ли он пользователей. """Проверяет, существует ли файл users.json и содержит ли он пользователей"""
Возвращает информацию о состоянии файла и наличии пользователей.
Returns:
dict: Словарь с ключами:
- "exists" (bool): Существует ли файл.
- "has_users" (bool): Содержит ли файл пользователей.
- "message" (str): Сообщение с описанием результата.
Raises:
HTTPException: Если произошла ошибка при проверке файла (status_code=500).
"""
try: try:
file_path = "/etc/gonets/users.json" file_path = "/etc/gonets/users.json"
storage = JsonStorage(file_path=file_path) storage = JsonStorage(file_path=file_path)
@ -159,7 +90,7 @@ async def check_users_file(translations: Translations = Depends(get_translations
return { return {
"exists": False, "exists": False,
"has_users": False, "has_users": False,
"message": translations.gettext("Файл users.json не существует"), "message": "Файл users.json не существует"
} }
data = storage._read_data() data = storage._read_data()
@ -169,15 +100,15 @@ async def check_users_file(translations: Translations = Depends(get_translations
return { return {
"exists": True, "exists": True,
"has_users": True, "has_users": True,
"message": translations.gettext("Файл users.json существует и содержит пользователей"), "message": "Файл users.json существует и содержит пользователей"
} }
else: else:
logger.info(f"Файл {file_path} существует, но не содержит пользователей") logger.info(f"Файл {file_path} существует, но не содержит пользователей")
return { return {
"exists": True, "exists": True,
"has_users": False, "has_users": False,
"message": translations.gettext("Файл users.json существует, но не содержит пользователей"), "message": "Файл users.json существует, но не содержит пользователей"
} }
except Exception as e: except Exception as e:
logger.error(f"Ошибка при проверке файла: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при проверке файла")) raise HTTPException(status_code=500, detail="Ошибка при проверке файла")

View file

@ -31,9 +31,7 @@ class SuccessResponse(BaseModel):
@router.get( @router.get(
"/zones", "/zones",
response_model=List[str], response_model=List[str],
responses={500: { responses={500: {"model": ErrorResponse}},
"model": ErrorResponse
}},
summary="Get all available zones", summary="Get all available zones",
) )
async def get_zones() -> List[str]: async def get_zones() -> List[str]:
@ -55,9 +53,7 @@ async def get_zones() -> List[str]:
@router.get( @router.get(
"/zones/default", "/zones/default",
response_model=str, response_model=str,
responses={500: { responses={500: {"model": ErrorResponse}},
"model": ErrorResponse
}},
summary="Get default zone", summary="Get default zone",
) )
async def get_default_zone() -> str: async def get_default_zone() -> str:
@ -79,14 +75,7 @@ async def get_default_zone() -> str:
@router.put( @router.put(
"/zones/default/{zone}", "/zones/default/{zone}",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={ responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Set default zone", summary="Set default zone",
) )
async def set_default_zone( async def set_default_zone(
@ -105,8 +94,9 @@ async def set_default_zone(
firewalld.set_default_zone(zone) firewalld.set_default_zone(zone)
return SuccessResponse(message=f"Default zone set to {zone}") return SuccessResponse(message=f"Default zone set to {zone}")
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, raise HTTPException(
detail=f"Zone '{zone}' not found") status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -117,18 +107,10 @@ async def set_default_zone(
@router.get( @router.get(
"/zones/{zone}", "/zones/{zone}",
response_model=FirewallZone, response_model=FirewallZone,
responses={ responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Get zone configuration", summary="Get zone configuration",
) )
async def get_zone(zone: str = Path(..., async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZone:
description="Zone name")) -> FirewallZone:
""" """
Get detailed configuration for a specific zone. Get detailed configuration for a specific zone.
@ -140,13 +122,16 @@ async def get_zone(zone: str = Path(...,
""" """
try: try:
zone_info = firewalld.get_zone_settings(zone) zone_info = firewalld.get_zone_settings(zone)
return FirewallZone(name=zone, return FirewallZone(
target=FirewallTarget.DEFAULT, name=zone,
interfaces=[], target=FirewallTarget.DEFAULT,
**zone_info) interfaces=[],
**zone_info
)
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, raise HTTPException(
detail=f"Zone '{zone}' not found") status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -157,13 +142,10 @@ async def get_zone(zone: str = Path(...,
@router.get( @router.get(
"/zones/{zone}/services", "/zones/{zone}/services",
response_model=List[str], response_model=List[str],
responses={500: { responses={500: {"model": ErrorResponse}},
"model": ErrorResponse
}},
summary="Get services in zone", summary="Get services in zone",
) )
async def get_services(zone: str = Path(..., async def get_services(zone: str = Path(..., description="Zone name")) -> List[str]:
description="Zone name")) -> List[str]:
""" """
Get all services in a zone. Get all services in a zone.
@ -185,14 +167,7 @@ async def get_services(zone: str = Path(...,
@router.post( @router.post(
"/zones/{zone}/services", "/zones/{zone}/services",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={ responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Add service to zone", summary="Add service to zone",
) )
async def add_service( async def add_service(
@ -211,11 +186,11 @@ async def add_service(
""" """
try: try:
firewalld.add_service(zone, rule.name, rule.timeout) firewalld.add_service(zone, rule.name, rule.timeout)
return SuccessResponse( return SuccessResponse(message=f"Service {rule.name} added to zone {zone}")
message=f"Service {rule.name} added to zone {zone}")
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, raise HTTPException(
detail=f"Zone '{zone}' not found") status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -226,19 +201,12 @@ async def add_service(
@router.delete( @router.delete(
"/zones/{zone}/services/{service}", "/zones/{zone}/services/{service}",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={ responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove service from zone", summary="Remove service from zone",
) )
async def remove_service( async def remove_service(
zone: str = Path(..., description="Zone name"), zone: str = Path(..., description="Zone name"),
service: str = Path(..., description="Service name"), service: str = Path(..., description="Service name"),
) -> SuccessResponse: ) -> SuccessResponse:
""" """
Remove a service from a zone. Remove a service from a zone.
@ -252,8 +220,7 @@ async def remove_service(
""" """
try: try:
firewalld.remove_service(zone, service) firewalld.remove_service(zone, service)
return SuccessResponse( return SuccessResponse(message=f"Service {service} removed from zone {zone}")
message=f"Service {service} removed from zone {zone}")
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -269,13 +236,10 @@ async def remove_service(
@router.get( @router.get(
"/zones/{zone}/ports", "/zones/{zone}/ports",
response_model=List[str], response_model=List[str],
responses={500: { responses={500: {"model": ErrorResponse}},
"model": ErrorResponse
}},
summary="Get ports in zone", summary="Get ports in zone",
) )
async def get_ports(zone: str = Path(..., async def get_ports(zone: str = Path(..., description="Zone name")) -> List[str]:
description="Zone name")) -> List[str]:
""" """
Get all ports in a zone. Get all ports in a zone.
@ -298,15 +262,9 @@ async def get_ports(zone: str = Path(...,
"/zones/{zone}/ports", "/zones/{zone}/ports",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={ responses={
400: { 400: {"model": ErrorResponse},
"model": ErrorResponse 404: {"model": ErrorResponse},
}, 500: {"model": ErrorResponse},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
},
}, },
summary="Add port to zone", summary="Add port to zone",
) )
@ -327,11 +285,10 @@ async def add_port(
try: try:
firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout) firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout)
return SuccessResponse( return SuccessResponse(
message= message=f"Port {rule.port}/{rule.protocol.value} added to zone {zone}"
f"Port {rule.port}/{rule.protocol.value} added to zone {zone}") )
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
detail=str(e))
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -342,14 +299,7 @@ async def add_port(
@router.delete( @router.delete(
"/zones/{zone}/ports", "/zones/{zone}/ports",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={ responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove port from zone", summary="Remove port from zone",
) )
async def remove_port( async def remove_port(
@ -369,11 +319,12 @@ async def remove_port(
try: try:
firewalld.remove_port(zone, rule.port, rule.protocol.value) firewalld.remove_port(zone, rule.port, rule.protocol.value)
return SuccessResponse( return SuccessResponse(
message= message=f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}"
f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}") )
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, raise HTTPException(
detail=f"Zone '{zone}' not found") status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -384,13 +335,10 @@ async def remove_port(
@router.get( @router.get(
"/zones/{zone}/sources", "/zones/{zone}/sources",
response_model=List[str], response_model=List[str],
responses={500: { responses={500: {"model": ErrorResponse}},
"model": ErrorResponse
}},
summary="Get sources in zone", summary="Get sources in zone",
) )
async def get_sources(zone: str = Path(..., async def get_sources(zone: str = Path(..., description="Zone name")) -> List[str]:
description="Zone name")) -> List[str]:
""" """
Get all sources in a zone. Get all sources in a zone.
@ -412,14 +360,7 @@ async def get_sources(zone: str = Path(...,
@router.post( @router.post(
"/zones/{zone}/sources", "/zones/{zone}/sources",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={ responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Add source to zone", summary="Add source to zone",
) )
async def add_source( async def add_source(
@ -438,11 +379,11 @@ async def add_source(
""" """
try: try:
firewalld.add_source(zone, rule.address) firewalld.add_source(zone, rule.address)
return SuccessResponse( return SuccessResponse(message=f"Source {rule.address} added to zone {zone}")
message=f"Source {rule.address} added to zone {zone}")
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, raise HTTPException(
detail=f"Zone '{zone}' not found") status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -453,14 +394,7 @@ async def add_source(
@router.delete( @router.delete(
"/zones/{zone}/sources/{source}", "/zones/{zone}/sources/{source}",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={ responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove source from zone", summary="Remove source from zone",
) )
async def remove_source( async def remove_source(
@ -479,8 +413,7 @@ async def remove_source(
""" """
try: try:
firewalld.remove_source(zone, source) firewalld.remove_source(zone, source)
return SuccessResponse( return SuccessResponse(message=f"Source {source} removed from zone {zone}")
message=f"Source {source} removed from zone {zone}")
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -496,9 +429,7 @@ async def remove_source(
@router.post( @router.post(
"/reload", "/reload",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={500: { responses={500: {"model": ErrorResponse}},
"model": ErrorResponse
}},
summary="Reload firewall rules", summary="Reload firewall rules",
) )
async def reload_firewall() -> SuccessResponse: async def reload_firewall() -> SuccessResponse:

View file

@ -1,111 +1,37 @@
from typing import Dict, Any from typing import Dict, Any
from fastapi import APIRouter, Body, HTTPException, Depends from fastapi import APIRouter, Body, HTTPException
from services.ip_service import IPService from services.ip_service import IPService
from utils.i18n import get_translations
from babel.support import Translations
router = APIRouter() router = APIRouter()
ip_service = IPService() ip_service = IPService()
@router.post("/check_ip") @router.post("/check_ip")
async def check_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)): async def check_ip_config(data: Dict[str, Any] = Body(...)):
"""Проверяет доступность IP-адреса перед его применением. """ Проверяет доступность IP-адреса """
Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес,
и возвращает результат проверки.
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса.
translations: Объект переводов для текущего языка.
Returns:
Dict[str, Any]: Словарь с ключами:
- "available" (bool): Доступен ли IP-адрес.
- "message" (str): Сообщение о результате проверки.
Raises:
HTTPException: Если произошла внутренняя ошибка (status_code=500).
"""
try: try:
result = await ip_service.check_ip_config(data, translations) result = await ip_service.check_ip_config(data)
return result return result
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
status_code=500,
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)
@router.post("/apply_ip") @router.post("/apply_ip")
async def apply_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)): async def apply_ip_config(data: Dict[str, Any] = Body(...)):
"""Применяет проверенные IP-настройки к указанному сетевому профилю. """ Применяет проверенные IP-настройки """
Обновляет настройки соединения через NetworkManager, отключает и
переподключает устройство к сети.
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "success" (bool): Успешность операции.
- "message" (str): Сообщение об успешном применении настроек.
Raises:
HTTPException: Если подключение не найдено (status_code=404) или
произошла внутренняя ошибка (status_code=500).
"""
try: try:
await ip_service.apply_ip_config(data, translations) await ip_service.apply_ip_config(data)
return { return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
"success": True,
"message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
connection_id=data["connection_id"]
),
}
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=translations.gettext(str(e))) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
status_code=500,
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)
@router.post("/save_ip") @router.post("/save_ip")
async def save_ip_config(data: Dict[str, Any] = Body(...), translations: Translations = Depends(get_translations)): async def save_ip_config(data: Dict[str, Any] = Body(...)):
"""Применяет проверенные IP-настройки для отката сетевого профиля. """ Применяет проверенные IP-настройки (Нужен для того, чтобы откатить настройки сетевого профиля обратно) """
Используется для сохранения и отката настроек сетевого профиля к предыдущему
состоянию через NetworkManager.
Args:
data (Dict[str, Any]): Словарь с данными конфигурации, включая
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "success" (bool): Успешность операции.
- "message" (str): Сообщение об успешном применении настроек.
Raises:
HTTPException: Если подключение не найдено (status_code=404) или
произошла внутренняя ошибка (status_code=500).
"""
try: try:
await ip_service.apply_ip_config(data, translations) await ip_service.apply_ip_config(data)
return { return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
"success": True,
"message": translations.gettext("Настройки сетевого профиля для {connection_id} применены").format(
connection_id=data["connection_id"]
),
}
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=translations.gettext(str(e))) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
status_code=500,
detail=translations.gettext("Ошибка проверки IP конфигурации: {error}").format(error=str(e))
)

View file

@ -1,16 +1,9 @@
from fastapi import APIRouter, Body, HTTPException, Depends from fastapi import APIRouter, Body, HTTPException
import logging import logging
from typing import Dict, Tuple, List, Union from typing import Dict, Tuple, List, Union
from services.network_service import NetworkService from services.network_service import NetworkService
from models.network import ( from models.network import NetworkInterfacesContainer, Activation, Deactivation, NetworkInterface, WiFiDevice
NetworkInterfacesContainer,
Activation,
Deactivation,
NetworkInterface,
WiFiDevice,
)
from utils.i18n import get_translations, _, _load_translations
from babel.support import Translations
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -18,208 +11,118 @@ logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
network_service = NetworkService() network_service = NetworkService()
class ContainerPositionManager:
"""Класс для управления позициями и состоянием контейнеров.
Хранит и обновляет координаты (x, y) и состояние (isOpen) для контейнеров class ContainerPositionManager:
активации и деактивации. """
Класс для управления позициями и состоянием контейнеров
""" """
def __init__(self): def __init__(self):
"""Инициализирует менеджер позиций контейнеров.
Создаёт начальные позиции и состояния для контейнеров "activation" и
"deactivation" с координатами (0, 0) и закрытым состоянием (False).
"""
self.positions: Dict[str, Dict[str, Union[int, bool]]] = { self.positions: Dict[str, Dict[str, Union[int, bool]]] = {
"activation": { 'activation': {'x': 0, 'y': 0, 'isOpen': False},
"x": 0, 'deactivation': {'x': 0, 'y': 0, 'isOpen': False}
"y": 0,
"isOpen": False
},
"deactivation": {
"x": 0,
"y": 0,
"isOpen": False
},
} }
def get_position_and_state(self, container_name: str, translations: Translations) -> Tuple[int, int, bool]: def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]:
"""Получает текущие координаты и состояние указанного контейнера.
Args:
container_name (str): Имя контейнера ("activation" или "deactivation").
translations: Объект переводов для текущего языка.
Returns:
Tuple[int, int, bool]: Кортеж с координатами (x, y) и состоянием (isOpen).
Raises:
ValueError: Если указан неверный идентификатор контейнера.
"""
container = self.positions.get(container_name) container = self.positions.get(container_name)
if not container: if not container:
raise ValueError(translations.gettext("Неверное имя контейнера: {container_name}").format(container_name=container_name)) raise ValueError(f"Неверное имя контейнера: {container_name}")
return container["x"], container["y"], container["isOpen"] return container['x'], container['y'], container['isOpen']
def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None: def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None:
"""Обновляет позицию и состояние контейнера по его идентификатору. container_name = 'activation' if container_id == 1 else 'deactivation'
self.positions[container_name] = {'x': x, 'y': y, 'isOpen': isOpen}
Args:
container_id (int): Идентификатор контейнера (1 для "activation", 2 для "deactivation").
x (int): Новая координата X.
y (int): Новая координата Y.
isOpen (bool): Новое состояние контейнера (открыт/закрыт).
Raises:
ValueError: Если container_id не соответствует ни одному контейнеру.
"""
container_name = "activation" if container_id == 1 else "deactivation"
self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen}
container_manager = ContainerPositionManager() container_manager = ContainerPositionManager()
async def get_network_data(translations: Translations = Depends(get_translations)) -> Dict[str, Dict]:
"""Возвращает текущие сетевые данные в виде словаря.
Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные, async def get_network_data() -> Dict[str, Dict]:
и возвращает их в формате Activation и Deactivation с учётом позиций и состояний
контейнеров.
Args:
translations: Объект переводов для текущего языка.
Returns:
Dict[str, Dict]: Словарь с ключами "activation" и "deactivation", содержащими
данные о соответствующих контейнерах.
Raises:
HTTPException: Если произошла ошибка при получении данных (status_code=500).
""" """
logger.debug("Calling get_network_data()") Возвращает текущие сетевые данные в виде словаря
"""
logger.debug("Вызов get_network_data()")
try: try:
network_data = await network_service.get_network_interfaces_container() network_data = await network_service.get_network_interfaces_container()
activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces) activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
activation_x, activation_y, activation_is_open = container_manager.get_position_and_state("activation", translations) activation_x, activation_y, activation_is_open = container_manager.get_position_and_state('activation')
deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state("deactivation", translations) deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state('deactivation')
activation = Activation( activation = Activation(
id=1, id=1,
interfaces=activation_interfaces, interfaces=activation_interfaces,
container_x=activation_x, container_x=activation_x,
container_y=activation_y, container_y=activation_y,
isOpen=activation_is_open, isOpen=activation_is_open
) )
deactivation = Deactivation( deactivation = Deactivation(
id=2, id=2,
interfaces=deactivation_interfaces, interfaces=deactivation_interfaces,
container_x=deactivation_x, container_x=deactivation_x,
container_y=deactivation_y, container_y=deactivation_y,
isOpen=deactivation_is_open, isOpen=deactivation_is_open
) )
return { return {
"activation": activation.model_dump(), "activation": activation.model_dump(),
"deactivation": deactivation.model_dump(), "deactivation": deactivation.model_dump()
} }
except Exception as e: except Exception as e:
logger.error(f"Error in get_network_data: {e}") logger.error(f"Ошибка в get_network_data: {e}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить сетевые данные")) raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные")
def _split_interfaces(
interfaces: List[Union[NetworkInterface, WiFiDevice]],
) -> Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
"""Разделяет интерфейсы на активные и неактивные.
Args: def _split_interfaces(interfaces: List[Union[NetworkInterface, WiFiDevice]]) -> Tuple[
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Список сетевых интерфейсов. List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
"""
Returns: Разделяет интерфейсы на активные и неактивные
Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
Кортеж из списков активных и неактивных интерфейсов.
""" """
activation_interfaces = [] activation_interfaces = []
deactivation_interfaces = [] deactivation_interfaces = []
for interface in interfaces: for interface in interfaces:
if interface.device_state == "100": if interface.device_state == '100':
activation_interfaces.append(interface) activation_interfaces.append(interface)
else: else:
deactivation_interfaces.append(interface) deactivation_interfaces.append(interface)
return activation_interfaces, deactivation_interfaces return activation_interfaces, deactivation_interfaces
@router.post("/update_container_position") @router.post("/update_container_position")
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...), translations: Translations = Depends(get_translations)): async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...)):
"""Обновляет позицию и состояние контейнера. """
Обновляет позицию и состояние контейнера
Принимает данные о новом положении и состоянии контейнера и обновляет их
в менеджере позиций.
Args:
data (Dict[str, Union[int, bool]]): Словарь с ключами:
- "container_id" (int): Идентификатор контейнера (1 или 2).
- "container_x" (int): Новая координата X.
- "container_y" (int): Новая координата Y.
- "isOpen" (bool): Новое состояние контейнера (открыт/закрыт).
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "success" (bool): Успешность операции.
- "message" (str): Сообщение об успешном обновлении.
Raises:
HTTPException: Если отсутствуют обязательные поля (status_code=400) или
типы данных неверны.
""" """
try: try:
required_keys = [
"container_id", "container_x", "container_y", "isOpen" required_keys = ["container_id", "container_x", "container_y", "isOpen"]
]
if not all(key in data for key in required_keys): if not all(key in data for key in required_keys):
raise ValueError( raise ValueError("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
translations.gettext("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
)
container_id = data["container_id"] container_id = data["container_id"]
container_x = data["container_x"] container_x = data["container_x"]
container_y = data["container_y"] container_y = data["container_y"]
is_open = data["isOpen"] is_open = data["isOpen"]
if (not isinstance(container_id, int) # Проверка типов
or not isinstance(container_x, int) if not isinstance(container_id, int) or not isinstance(container_x, int) or not isinstance(container_y,
or not isinstance(container_y, int) int) or not isinstance(
or not isinstance(is_open, bool)): is_open, bool):
raise ValueError(translations.gettext("Неверный тип данных в запросе")) raise ValueError("Неверный тип данных в запросе")
container_manager.update_position(container_id, container_x, container_y, is_open) container_manager.update_position(container_id, container_x, container_y, is_open)
return { return {"success": True, "message": "Позиция и состояние контейнера обновлены"}
"success": True,
"message": translations.gettext("Позиция и состояние контейнера обновлены")
}
except Exception as e: except Exception as e:
logger.error(f"Error updating container position: {e}") logger.error(f"Ошибка обновления позиции контейнера: {e}")
raise HTTPException(status_code=400, detail=translations.gettext("Не удалось обновить положение контейнера: {error}").format(error=str(e))) raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}")
@router.get("/get_hostname") @router.get("/get_hostname")
async def get_hostname(translations: Translations = Depends(get_translations)): async def get_hostname():
"""Возвращает имя хоста системы.
Выполняет запрос к сервису для получения имени хоста.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключом "hostname" и значением имени хоста.
Raises:
HTTPException: Если произошла ошибка при получении имени хоста (status_code=500).
"""
try: try:
hostname_info = await network_service.get_hostname() hostname_info = await network_service.get_hostname()
return hostname_info return hostname_info
except Exception as e: except Exception as e:
logger.error(f"Error in get_hostname: {e}") logger.error(f"Ошибка в get_hostname: {e}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось получить имя хоста")) raise HTTPException(status_code=500, detail="Не удалось получить имя хоста")

View file

@ -1,209 +0,0 @@
import os
import logging
import subprocess
from typing import Optional
from fastapi import APIRouter, HTTPException, Depends, Response, Request, Body
from starlette.responses import FileResponse
from services.settings_service import SettingsService
from utils.i18n import get_translations
from babel.support import Translations
from models.subnet import SubnetScanRequest # Предполагается, что модель определена
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
router = APIRouter()
settings_service = SettingsService()
@router.post("/clear_subnet")
def clear_subnet(subnet: Optional[str] = None, translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети, сохранённые в devices.json.
Удаляет все записи об узлах подсети, оставляя файл пустым.
Args:
subnet (Optional[str]): Подсеть в формате CIDR (например, "10.1.1.0/24").
Если None, очищает все подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
Raises:
HTTPException: Если произошла ошибка очистки (status_code=500).
"""
try:
success = settings_service.clear_subnet(subnet)
if success:
if subnet:
return {"message": translations.gettext("Данные узлов подсети {subnet} успешно очищены").format(subnet=subnet)}
else:
return {"message": translations.gettext("Данные всех подсетей успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить данные узлов подсети"))
except Exception as e:
logger.error(f"Error clearing subnet data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
@router.post("/clear_wifi")
def clear_wifi(translations: Translations = Depends(get_translations)):
"""Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json.
Удаляет все записи о точках доступа, оставляя файл пустым.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
Raises:
HTTPException: Если произошла ошибка очистки (status_code=500).
"""
try:
success = settings_service.clear_wifi()
if success:
return {"message": translations.gettext("Данные точек доступа Wi-Fi успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить данные точек доступа Wi-Fi"))
except Exception as e:
logger.error(f"Error clearing wifi data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
@router.post("/clear_all_data")
def clear_all_data(translations: Translations = Depends(get_translations)):
"""Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах.
Удаляет все записи из devices.json и wifi.json.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешной очистке.
Raises:
HTTPException: Если произошла ошибка очистки (status_code=500).
"""
try:
success = settings_service.clear_all_data()
if success:
return {"message": translations.gettext("Все сетевые данные успешно очищены")}
else:
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось очистить все сетевые данные"))
except Exception as e:
logger.error(f"Error clearing all data: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка очистки: {error}").format(error=str(e)))
WIFI_PATH = "storage/wifi/wifi.json"
@router.post("/export_devices", response_class=FileResponse)
async def export_devices(request: SubnetScanRequest = Body(...), translations: Translations = Depends(get_translations)):
"""
Экспортирует файл с данными устройств в формате JSON для указанной подсети.
Args:
request (SubnetScanRequest): Объект с полем subnet в формате CIDR.
translations: Объект переводов для текущего языка.
Returns:
FileResponse: Файл <subnet>.json для скачивания.
Raises:
HTTPException: Если файл не найден (status_code=404).
"""
subnet = request.subnet
file_path = f"storage/subnets/{subnet.replace('/', '_')}.json"
if not os.path.exists(file_path):
logger.warning(f"Export file not found: {file_path}")
raise HTTPException(status_code=404, detail=translations.gettext("Файл для подсети {subnet} не найден").format(subnet=subnet))
logger.info(f"Exporting file: {file_path}")
return FileResponse(
path=file_path,
filename=f"{subnet.replace('/', '_')}.json",
media_type="application/json"
)
@router.get("/export_wifi", response_class=FileResponse)
async def export_wifi(translations: Translations = Depends(get_translations)):
"""
Экспортирует файл с данными устройств в формате JSON.
Args:
translations: Объект переводов для текущего языка.
Returns:
FileResponse: Файл wifi.json для скачивания.
Raises:
HTTPException: Если файл не найден (status_code=404).
"""
if not os.path.exists(WIFI_PATH):
logger.warning(f"Export file not found: {WIFI_PATH}")
raise HTTPException(status_code=404, detail=translations.gettext("Файл не найден"))
logger.info(f"Exporting file: {WIFI_PATH}")
return FileResponse(
path=WIFI_PATH,
filename="wifi.json",
media_type="application/json"
)
@router.post("/set_language")
def set_language(locale: str, response: Response, translations: Translations = Depends(get_translations)):
"""
Устанавливает куку locale для смены языка пользователя.
Args:
locale (str): Используемый язык.
response (Response): Запрос.
translations: Объект переводов текущего языка.
Returns:
dict: Сообщение об успешной установке языка.
"""
lc_messages_path = os.path.join(os.path.dirname(__file__), '..', '..', 'locales', locale, 'LC_MESSAGES')
if not os.path.isdir(lc_messages_path):
raise HTTPException(
status_code=400,
detail=translations.gettext("Язык '{locale}' не поддерживается.").format(locale=locale)
)
has_mo_or_po = any(f.endswith('.mo') or f.endswith('.po') for f in os.listdir(lc_messages_path))
if not has_mo_or_po:
raise HTTPException(
status_code=400,
detail=translations.gettext("Язык '{locale}' не поддерживается (нет файлов переводов).").format(locale=locale)
)
response.set_cookie(key="locale", value=locale, path="/", httponly=True)
return {"message": translations.gettext("Язык успешно установлен: {locale}").format(locale=locale)}
@router.get("/get_system_language")
async def get_system_language(request: Request, translations: Translations = Depends(get_translations)):
"""Выводит текущий язык, установленный в системе на основе locale.
Выполняет команду 'locale' в системе и извлекает параметры LANG
Args:
request (Request): Объект запроса для доступа к параметрам.
translations: Объект переводов текущего языка.
Returns:
dict: Словарь с текущим языком системы.
Raises:
HTTPException: Если не удалось извлечь системный язык (status_code=500).
"""
try:
result = subprocess.run(['locale'], capture_output=True, text=True, check=True)
output = result.stdout
lang = next((line.split('=')[1].strip() for line in output.splitlines() if line.startswith('LANG=')), None)
if lang:
return {"language": lang}
else:
logger.warning(f"No locale found: {output}")
return {"language": 'unknown'}
except subprocess.CalledProcessError as e:
logger.error(f"Error executing locale command: {e}")
raise HTTPException(status_code=500, detail=translations.gettext("Не удалось определить язык в системе"))
except Exception as e:
logger.error(f"Unexpected error getting system language: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка при определении языка в системе {error}").format(error=str(e)))

View file

@ -1,352 +0,0 @@
from fastapi import APIRouter, HTTPException, Body, Depends
from models.subnet import SubnetScanResult, SubnetScanRequest
from models.wifi import WifiScanResult
from models.request import NoteRequest, WifiConnectRequest, WifiDisconnectRequest
from services.subnet_service import SubnetService, get_subnet_service
from services.wifi_service import WifiService
from services.connect_service import WifiConnectionService
from utils.i18n import get_translations, _
from babel.support import Translations
import logging
router = APIRouter()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@router.post("/devices", response_model=SubnetScanResult)
def scan_subnet(request: SubnetScanRequest = Body(...), subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Сканирует локальную подсеть и возвращает данные об устройствах.
Выполняет сканирование указанной подсети для обнаружения активных устройств.
Args:
request (SubnetScanRequest): Объект с полем subnet.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
SubnetScanResult: Объект с результатами сканирования (устройства и их данные).
Raises:
HTTPException: Если произошла ошибка сканирования (status_code=500).
"""
try:
# Если subnet пустая строка, определяем автоматически
subnet = request.subnet.strip() if request.subnet else None
logger.info(f"Requested subnet scan for: {subnet or 'auto-detected'}")
result = subnet_service.scan_subnet(subnet)
if not result.devices and result.error:
logger.warning(f"No devices found in subnet {subnet or 'auto-detected'}")
return result
except Exception as e:
logger.error(f"Error in scan_subnet: {str(e)}")
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования подсети: {error}").format(error=str(e)))
@router.get("/wifi", response_model=WifiScanResult)
def scan_wifi(timeout: int = 30, translations: Translations = Depends(get_translations)):
"""Сканирует Wi-Fi точки доступа и возвращает данные о них.
Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом.
Args:
timeout (int, optional): Таймаут сканирования в секундах. По умолчанию 30.
translations: Объект переводов для текущего языка.
Returns:
WifiScanResult: Объект с результатами сканирования (список Wi-Fi сетей).
Raises:
HTTPException: Если произошла ошибка сканирования Wi-Fi (status_code=500).
"""
try:
wifi_service = WifiService()
result = wifi_service.scan_wifi(timeout=timeout)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка сканирования Wi-Fi: {error}").format(error=str(e)))
@router.post("/devices/{mac}/note")
def add_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Добавляет заметку для устройства по MAC-адресу.
Привязывает заметку к устройству, идентифицируемому по MAC-адресу.
Args:
mac (str): MAC-адрес устройства.
request (NoteRequest): Объект с полем "note" для текста заметки.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном добавлении заметки.
Raises:
HTTPException: Если устройство не найдено (status_code=404), заметка уже
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
success, result = subnet_service.add_note(mac, request.note, translations)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} уже существует").format(mac=mac))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' добавлена для устройства {mac}").format(note=request.note, mac=mac)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
@router.put("/devices/{mac}/note")
def update_note(mac: str, request: NoteRequest, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Обновляет заметку для устройства по MAC-адресу.
Изменяет существующую заметку, привязанную к устройству.
Args:
mac (str): MAC-адрес устройства.
request (NoteRequest): Объект с полем "note" для нового текста заметки.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном обновлении заметки.
Raises:
HTTPException: Если устройство не найдено (status_code=404), заметка не
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
success, result = subnet_service.update_note(mac, request.note, translations)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=mac))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' обновлена для устройства {mac}").format(note=request.note, mac=mac)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
@router.delete("/devices/{mac}/note")
def delete_note(mac: str, subnet_service: SubnetService = Depends(get_subnet_service), translations: Translations = Depends(get_translations)):
"""Удаляет заметку для устройства по MAC-адресу.
Удаляет привязанную заметку для указанного устройства.
Args:
mac (str): MAC-адрес устройства.
subnet_service: Сервис сканирования подсети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном удалении заметки.
Raises:
HTTPException: Если устройство не найдено (status_code=404), заметка не
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
success, result = subnet_service.delete_note(mac, translations)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Устройство с MAC {mac} не найдено").format(mac=mac))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для устройства с MAC {mac} не существует").format(mac=mac))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка удалена для устройства {mac}").format(mac=mac)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
@router.post("/wifi/{ssid}/note")
def add_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
"""Добавляет заметку для сети Wi-Fi по SSID.
Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID.
Args:
ssid (str): Идентификатор Wi-Fi сети.
request (NoteRequest): Объект с полем "note" для текста заметки.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном добавлении заметки.
Raises:
HTTPException: Если сеть не найдена (status_code=404), заметка уже
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
wifi_service = WifiService()
success, result = wifi_service.add_note(ssid, request.note)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} уже существует").format(ssid=ssid))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' добавлена для сети с SSID {ssid}").format(note=request.note, ssid=ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка добавления заметки: {error}").format(error=str(e)))
@router.put("/wifi/{ssid}/note")
def update_wifi_note(ssid: str, request: NoteRequest, translations: Translations = Depends(get_translations)):
"""Обновляет заметку для сети Wi-Fi по SSID.
Изменяет существующую заметку, привязанную к Wi-Fi сети.
Args:
ssid (str): Идентификатор Wi-Fi сети.
request (NoteRequest): Объект с полем "note" для нового текста заметки.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном обновлении заметки.
Raises:
HTTPException: Если сеть не найдена (status_code=404), заметка не
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
wifi_service = WifiService()
success, result = wifi_service.update_note(ssid, request.note)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(ssid=ssid))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка '{note}' обновлена для сети с SSID {ssid}").format(note=request.note, ssid=ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка обновления заметки: {error}").format(error=str(e)))
@router.delete("/wifi/{ssid}/note")
def delete_wifi_note(ssid: str, translations: Translations = Depends(get_translations)):
"""Удаляет заметку для сети Wi-Fi по SSID.
Удаляет привязанную заметку для указанной Wi-Fi сети.
Args:
ssid (str): Идентификатор Wi-Fi сети.
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном удалении заметки.
Raises:
HTTPException: Если сеть не найдена (status_code=404), заметка не
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
"""
try:
wifi_service = WifiService()
success, result = wifi_service.delete_note(ssid)
if not success:
if result[0] == "not_found":
raise HTTPException(status_code=404, detail=translations.gettext("Сеть с SSID {ssid} не найдена").format(ssid=ssid))
elif result[0] == "note_not_exists":
raise HTTPException(status_code=400, detail=translations.gettext("Заметка для сети с SSID {ssid} не существует").format(ssid=ssid))
else:
raise HTTPException(status_code=500, detail=translations.gettext("Внутренняя ошибка сервера: {error}").format(error=result[1]["error"]))
return {"message": translations.gettext("Заметка удалена для сети с SSID {ssid}").format(ssid=ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка удаления заметки: {error}").format(error=str(e)))
@router.post("/wifi/connect")
def connect_to_wifi(request: WifiConnectRequest, translations: Translations = Depends(get_translations)):
"""Подключается к Wi-Fi сети по SSID и паролю.
Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных
учетных данных.
Args:
request (WifiConnectRequest): Объект с полями "ssid" и "password".
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном подключении.
Raises:
HTTPException: Если подключение не удалось (status_code=400).
"""
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.connect(request.ssid, request.password)
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"message": translations.gettext("Успешно подключено к {ssid}").format(ssid=request.ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка подключения к Wi-Fi: {error}").format(error=str(e)))
@router.post("/wifi/disconnect")
def disconnect_from_wifi(request: WifiDisconnectRequest, translations: Translations = Depends(get_translations)):
"""Отключается от Wi-Fi сети по указанному SSID.
Выполняет отключение от указанной Wi-Fi сети.
Args:
request (WifiDisconnectRequest): Объект с полем "ssid".
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с сообщением об успешном отключении.
Raises:
HTTPException: Если отключение не удалось (status_code=400).
"""
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.disconnect(request.ssid)
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"message": translations.gettext(message or "Успешно отключено от {ssid}").format(ssid=request.ssid)}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка отключения от Wi-Fi: {error}").format(error=str(e)))
@router.get("/wifi/status")
def get_wifi_status(translations: Translations = Depends(get_translations)):
"""Проверяет статус текущего подключения к Wi-Fi.
Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно.
Args:
translations: Объект переводов для текущего языка.
Returns:
dict: Словарь с ключами:
- "status" (str): Состояние ("connected" или другое).
- "ssid" (str, optional): SSID текущей сети, если подключено.
Raises:
HTTPException: Если проверка статуса не удалась (status_code=400).
"""
try:
wifi_service = WifiConnectionService(translations)
success, message = wifi_service.get_status()
if not success:
raise HTTPException(status_code=400, detail=translations.gettext(message))
return {"status": translations.gettext("connected"), "ssid": message}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=translations.gettext("Ошибка проверки статуса Wi-Fi: {error}").format(error=str(e)))

Some files were not shown because too many files have changed in this diff Show more