diff --git a/__pycache__/web_server.cpython-311.pyc b/__pycache__/web_server.cpython-311.pyc index dfc2891..7fc74e7 100644 Binary files a/__pycache__/web_server.cpython-311.pyc and b/__pycache__/web_server.cpython-311.pyc differ diff --git a/callbacks/__pycache__/app.cpython-311.pyc b/callbacks/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..9b83265 Binary files /dev/null and b/callbacks/__pycache__/app.cpython-311.pyc differ diff --git a/callbacks/app.py b/callbacks/app.py new file mode 100644 index 0000000..65591bb --- /dev/null +++ b/callbacks/app.py @@ -0,0 +1,44 @@ +import subprocess +from pathlib import Path +from typing import Dict, Any + +async def run_app(args: Dict[str, Any]) -> Dict[str, Any]: + """ + Запускает приложение или ярлык по указанному пути. + + Args: + args: Dictionary containing 'url' (string) of the website to open. + + Returns: + Dictionary with status message and input arguments. + + Raises: + OSError: If opening the website fails. + """ + try: + path = args.get("path") + # Проверка существования файла + if not Path(path).exists(): + raise OSError(f"Файл не найден: {path}") + + # Проверка расширения файла + if not path.lower().endswith(('.exe', '.lnk', '.bat')): + raise OSError(f"Файл {path} может не быть исполняемым файлом или ярлыком") + + # Попытка запуска приложения + subprocess.run(['start', '', path], shell=True, check=True) + return { + "message": f"App {path} opened successfully", + "args": args + } + + except FileNotFoundError as e: + raise OSError(f"Файл не найден или путь некорректен: {str(e)}") + except subprocess.CalledProcessError as e: + raise OSError(f"Ошибка при запуске приложения: {str(e)}") + except PermissionError as e: + raise OSError(f"Недостаточно прав для запуска: {str(e)}") + except OSError as e: + raise OSError(f"Ошибка операционной системы при запуске: {str(e)}") + except Exception as e: + raise OSError(f"Непредвиденная ошибка: {str(e)}") \ No newline at end of file diff --git a/config/frontend.yaml b/config/frontend.yaml new file mode 100644 index 0000000..76e27ed --- /dev/null +++ b/config/frontend.yaml @@ -0,0 +1,71 @@ +- type: "break" # Разделитель с подписью "система" + label: "Система" + +- type: "clock" # Часы + mini_icon: "/icons/mini/clock-five.png" + +- type: "gauge" # Круговые индикаторы + gauges: + - value: 0 + label: "Загрузка CPU" + - value: 0 + label: "Занято RAM" + tag: "system_load" + mini_icon: "/icons/mini/chart-line-up.png" + +- type: "number" # Вывод числа + value: 0 + label: "Трафик Mbit/s" + tag: "net_traffic" + mini_icon: "/icons/mini/wifi.png" + +- type: "slider" # Слайдеры + label: "Настройки" + sliders: + - value: 0 + label: "Яркость экрана" + action: "screen.chenge_brightness" + - value: 0 + label: "Громкость звука" + action: "media.set_volume" + mini_icon: "/icons/mini/settings.png" + +- type: "break" # Разделитель + label: "Сайты" + +- type: "image" # Кнопка с картинкой + imageUrl: "/icons/telegram-logo-svgrepo-com.svg" + label: "Telegram" + action: "web.open_url" + action_args: + url: "https://ya.ru " + mini_icon: "/icons/mini/arrow-up-right-from-square.png" + +- type: "image" # Кнопка с картинкой + imageUrl: "/icons/cloud.png" + label: "Cloud" + action: "web.open_url" + action_args: + url: "https://cloud.viadev.su " + mini_icon: "/icons/mini/arrow-up-right-from-square.png" + +- type: "break" # Разделитель + label: "Приложения" + +- type: "image" # Кнопка с картинкой + imageUrl: "/icons/steam-svgrepo-com.svg" + label: "Steam" + action: "app.run_app" + action_args: + path: "C:/Users/Slava/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Steam/Steam.lnk" + mini_icon: "/icons/mini/arrow-up-right-from-square.png" + + +- type: "break" # Разделитель + label: "Управление ПК" + +- type: "image" # Кнопка с картинкой + imageUrl: "/icons/vpn.png" + label: "VPN" + action: "v2ray.enable_vpn" + action_args: {} \ No newline at end of file diff --git a/static/frontend/.env b/static/frontend/.env new file mode 100644 index 0000000..425c60f --- /dev/null +++ b/static/frontend/.env @@ -0,0 +1 @@ +VITE_API_PORT = 8000 \ No newline at end of file diff --git a/static/frontend/package-lock.json b/static/frontend/package-lock.json index 71d65bd..89bc707 100644 --- a/static/frontend/package-lock.json +++ b/static/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.7", "axios": "^1.9.0", + "qrcode.react": "^4.2.0", "react": "^19.1.0", "react-dom": "^19.1.0", "tailwindcss": "^4.1.7" @@ -3247,6 +3248,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", diff --git a/static/frontend/package.json b/static/frontend/package.json index 0b74ea6..0ee59e8 100644 --- a/static/frontend/package.json +++ b/static/frontend/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.7", "axios": "^1.9.0", + "qrcode.react": "^4.2.0", "react": "^19.1.0", "react-dom": "^19.1.0", "tailwindcss": "^4.1.7" diff --git a/static/frontend/src/Api.jsx b/static/frontend/src/Api.jsx index b50174d..a83f2ff 100644 --- a/static/frontend/src/Api.jsx +++ b/static/frontend/src/Api.jsx @@ -1,9 +1,9 @@ import axios from 'axios'; -const API_URL = "http://192.168.2.151:8000" -const createSha256Hash = async (input) => { + +export const createSha256Hash = async (input) => { try { // Convert string to array buffer const msgBuffer = new TextEncoder().encode(input); @@ -23,11 +23,11 @@ const createSha256Hash = async (input) => { } -const sendRequest = async (action,args) => { +export const sendRequest = async (action,args) => { try { const TS = Math.round(Date.now()/1000); - const hash = await createSha256Hash(""+JSON.stringify(args)+TS); - const response = await axios.post(API_URL+"/action/"+action, {args:args,hash:hash+"."+TS}, { + const hash = await createSha256Hash(window.localStorage.auth+JSON.stringify(args)+TS); + const response = await axios.post("http://"+window.location.hostname+":"+import.meta.env.VITE_API_PORT+"/action/"+action, {args:args,hash:hash+"."+TS}, { timeout: 4000, headers: { 'Content-Type': 'application/json' @@ -40,4 +40,3 @@ const sendRequest = async (action,args) => { } }; -export default sendRequest; \ No newline at end of file diff --git a/static/frontend/src/App.css b/static/frontend/src/App.css index f8635fb..938c310 100644 --- a/static/frontend/src/App.css +++ b/static/frontend/src/App.css @@ -7,12 +7,17 @@ transition: all 0.3s ease; } -.glass:hover { - background: rgba(147, 51, 234, 0.2); - border-color: rgba(147, 51, 234, 0.7); - box-shadow: 0 6px 12px rgba(147, 51, 234, 0.3); +.glass { + &:hover, + &:focus, + &:active { + background: rgba(147, 51, 234, 0.2); + border-color: rgba(147, 51, 234, 0.7); + box-shadow: 0 6px 12px rgba(147, 51, 234, 0.3); + } } + @keyframes flipRight { 0% { transform: rotate(0deg); diff --git a/static/frontend/src/App.jsx b/static/frontend/src/App.jsx index 5b8bdfe..657362b 100644 --- a/static/frontend/src/App.jsx +++ b/static/frontend/src/App.jsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react' import './App.css' -import sendRequest from "./Api.jsx"; +import { sendRequest, createSha256Hash } from "./Api.jsx"; +import { QRCodeSVG } from 'qrcode.react'; +import axios from 'axios'; window.sendRequest = sendRequest; const TextTile = ({ label }) => ( @@ -94,25 +96,16 @@ const NumberTile = ({ value, label }) => ( ); const App = () => { - const [tiles, setTiles] = useState([ - { id: 0, type: "break", label: "Система" }, - { id: 1, type: "clock", mini_icon:"/icons/mini/clock-five.png" }, + const [phase, setPhase] = useState("loading"); + const [QrLink, setQrLink] = useState(); + const [Password, setPassword] = useState(""); - { id: 2, type: "gauge", gauges: [{ value: 3.60 * 0, label: "Загрузка CPU" }, { value: 3.60 * 0, label: "Занято RAM" }], tag: "system_load", mini_icon:"/icons/mini/chart-line-up.png" }, + const handlePasswordChange = (event) => { + setPassword(event.target.value); + }; - { id: 3, type: "number", value: 0, label: "Трафик Mbit/s", tag: "net_traffic", mini_icon:"/icons/mini/wifi.png" }, - { id: 4, type: "slider", label: "Настройки", sliders: [{ value: 50, label: "Яроксть экрана", action: "screen.chenge_brightness" }, { value: 75, label: "Громкость звука", action: "media.set_volume" }], mini_icon:"/icons/mini/settings.png" }, - - { id: 5, type: "break", label: "Сайты" }, - - { id: 6, type: "image", imageUrl: "/icons/steam-svgrepo-com.svg", label: "Steam", action: "web.open_url", action_args: { url: "https://ya.ru" }, mini_icon:"/icons/mini/arrow-up-right-from-square.png" }, - { id: 7, type: "image", imageUrl: "/icons/telegram-logo-svgrepo-com.svg", label: "Telegram", action: "web.open_url", action_args: { url: "https://ya.ru" }, mini_icon:"/icons/mini/arrow-up-right-from-square.png" }, - { id: 8, type: "image", imageUrl: "/icons/cloud.png", label: "Cloud", action: "web.open_url", action_args: { url: "https://cloud.viadev.su" }, mini_icon:"/icons/mini/arrow-up-right-from-square.png" }, - - { id: 9, type: "break", label: "Управление ПК" }, - { id: 10, type: "image", imageUrl: "/icons/vpn.png", label: "VPN", action: "v2ray.enable_vpn", action_args: {} }, - ]); + const [tiles, setTiles] = useState(); @@ -133,17 +126,6 @@ const App = () => { }; - - const toggleImageEnabled = (tileId) => { - setTiles(prevTiles => - prevTiles.map(tile => { - if (tile.id === tileId && tile.type === "image") { - return { ...tile, enabled: !tile.enabled }; - } - return tile; - }) - ); - }; const setTileProps = (tileId, props) => { setTiles(prevTiles => prevTiles.map(tile => { @@ -164,29 +146,88 @@ const App = () => { }) ); }; + const updateSystemStats = async () => { + try { + const data = await sendRequest("system.get_system_metrics", {}); + setTilePropsByTag("system_load", { + gauges: [ + { value: (3.60 * data.result.metrics.cpu_usage_percent).toFixed(0), label: "Загрузка CPU" }, + { value: (3.60 * (data.result.metrics.ram_used_gb / data.result.metrics.ram_total_gb) * 100).toFixed(0), label: "Занято RAM" } + ] + }) + setTilePropsByTag("net_traffic", { + value: data.result.metrics.network_traffic_mbps + }) + } catch (e) { + console.error(e); + } + }; - useEffect(() => { - const updateSystemStats = async () => { - try { - const data = await sendRequest("system.get_system_metrics", {}); - setTilePropsByTag("system_load", { - gauges: [ - { value: (3.60 * data.result.metrics.cpu_usage_percent).toFixed(0), label: "Загрузка CPU" }, - { value: (3.60 * (data.result.metrics.ram_used_gb / data.result.metrics.ram_total_gb) * 100).toFixed(0), label: "Занято RAM" } - ] - }) - setTilePropsByTag("net_traffic", { - value: data.result.metrics.network_traffic_mbps - }) + const TryAuth = async (passw) => { + try { + setPhase("loading"); + const hash = await createSha256Hash("frontend_config" + passw); + const response = await axios.get("http://" + window.location.hostname + ":" + import.meta.env.VITE_API_PORT + "/frontend-config", { + timeout: 4000, + params: { + hash: hash, + }, + headers: { + 'Content-Type': 'application/json' + } + }); + setTimeout(() => { + window.localStorage.auth = passw; + setTiles(response.data); + setPhase("dash"); + updateSystemStats(); + const interval = setInterval(updateSystemStats, 2000); + }, 500) - } catch (e) { - console.error(e); + } catch (e) { + window.localStorage.clear("auth") + window.location.reload(); + } + } + const RequestAuth = () => { + setPhase("login"); + } + const Init = async () => { + if (window.localStorage.auth) { + return TryAuth(window.localStorage.auth); + } + const response = await axios.get("http://" + window.location.hostname + ":" + import.meta.env.VITE_API_PORT + "/primary-ip", { + timeout: 4000, + headers: { + 'Content-Type': 'application/json' } - }; - updateSystemStats(); - const interval = setInterval(updateSystemStats, 2000); - return () => clearInterval(interval); + }); + console.log(response.data) + const currentUrl = new URL(window.location.href); + + // Меняем хост + currentUrl.hostname = response.data; + + // Устанавливаем hash + currentUrl.hash = '#skip_start'; + + // Получаем новый URL + const newUrl = currentUrl.toString(); + setQrLink(newUrl); + if (window.location.hash != "#skip_start") { + setPhase("start"); + } else { + setPhase("login"); + } + } + useEffect(() => { + + + Init(); + // updateSystemStats(); + // const interval = setInterval(updateSystemStats, 2000); + // return () => { clearInterval(interval) }; }, []); @@ -214,53 +255,111 @@ const App = () => { } } - return ( -
-
- {tiles.map(tile => ( -
- {tile.type === "break" ? ( -
{tile.label}
- ) : ( -
{ onTileClick(tile) }} - className={`glass rounded-lg ${tile.type === "gauge" || tile.type === "slider" || tile.type === "clock" ? "w-full h-45 lg:h-48" : "w-full h-45 lg:h-48"} relative ${tile.type === "image" && tile.enabled ? "glass_enabled" : ""}`} - > - {tile.loading && -
-
-
- } - {!tile.loading && - <> - {tile.type === "text" && } - {tile.type === "image" && ( - toggleImageEnabled(tile.id)} - /> - )} - {tile.type === "clock" && } - {tile.type === "gauge" && } - {tile.type === "slider" && } - {tile.type === "number" && } - {tile.mini_icon && ( -
- - - -
- )} - - } + if (phase === "start") { + return ( +
+
+
+
+
+
- )} +
- ))} +
+
+ + +
+
+
-
+ ) + } + + if (phase === "loading") { + return ( +
+
+
+ ) + } + + return ( + <> + {phase == "login" && ( +
+
+
{ TryAuth(Password) }} className='w-full'> + +
+
+
+ )} + {phase === "dash" && ( + +
+
+ {tiles.map(tile => ( +
+ {tile.type === "break" ? ( +
{tile.label}
+ ) : ( +
{ onTileClick(tile) }} + className={`glass rounded-lg ${tile.type === "gauge" || tile.type === "slider" || tile.type === "clock" ? "w-full h-45 lg:h-48" : "w-full h-45 lg:h-48"} relative ${tile.type === "image" && tile.enabled ? "glass_enabled" : ""}`} + > + {tile.loading && +
+
+
+ } + {!tile.loading && + <> + {tile.type === "text" && } + {tile.type === "image" && ( + + )} + {tile.type === "clock" && } + {tile.type === "gauge" && } + {tile.type === "slider" && } + {tile.type === "number" && } + {tile.mini_icon && ( +
+ + + +
+ )} + + } +
+ )} +
+ ))} +
+
+ + )}; + ); + }; + export default App diff --git a/static/frontend/src/index.css b/static/frontend/src/index.css index bf9f16f..03411dc 100644 --- a/static/frontend/src/index.css +++ b/static/frontend/src/index.css @@ -1,4 +1,23 @@ @import "tailwindcss"; -#root{ +html,body,#root{ width:100%; + height: 100%; + background-color: #101828; +} +@keyframes anim-pop { + 0% { + opacity: 0; + transform: scale(.95); + transform: scale(var(--btn-focus-scale, .98)); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.anim-pop { + opacity: 0; /* Element starts hidden */ + animation: anim-pop .3s ease-in forwards; + animation-delay: 0.2s; } \ No newline at end of file diff --git a/static/frontend/src/main.jsx b/static/frontend/src/main.jsx index ffc71ea..820156d 100644 --- a/static/frontend/src/main.jsx +++ b/static/frontend/src/main.jsx @@ -4,7 +4,7 @@ import './index.css' import App from './App.jsx' createRoot(document.getElementById('root')).render( - +
- +
) diff --git a/utils/__pycache__/get_primary_ip.cpython-311.pyc b/utils/__pycache__/get_primary_ip.cpython-311.pyc new file mode 100644 index 0000000..a40b416 Binary files /dev/null and b/utils/__pycache__/get_primary_ip.cpython-311.pyc differ diff --git a/utils/get_primary_ip.py b/utils/get_primary_ip.py new file mode 100644 index 0000000..3534394 --- /dev/null +++ b/utils/get_primary_ip.py @@ -0,0 +1,23 @@ +import netifaces + +def get_primary_ip(): + try: + # Получаем список всех интерфейсов + interfaces = netifaces.interfaces() + for iface in interfaces: + # Пропускаем loopback интерфейс + if iface == 'lo': + continue + # Получаем данные об интерфейсе + iface_data = netifaces.ifaddresses(iface) + # Проверяем наличие IPv4 адреса + if netifaces.AF_INET in iface_data: + for addr in iface_data[netifaces.AF_INET]: + ip = addr['addr'] + # Исключаем локальные адреса + if not ip.startswith('127.'): + return ip + return "IP не найден" + except Exception as e: + return f"Ошибка: {e}" + diff --git a/web_server.py b/web_server.py index 44526bf..d6b2d27 100644 --- a/web_server.py +++ b/web_server.py @@ -10,8 +10,11 @@ from fastapi.responses import JSONResponse import hashlib import json import time +import yaml +from pathlib import Path +from utils.get_primary_ip import get_primary_ip -PASSWORD = "" +PASSWORD = "10010055" @@ -29,11 +32,6 @@ app.add_middleware( # Static files & UI server app.mount("/static", StaticFiles(directory="static"), name="static") -# Pydantic model for request payload -class CommandModel(BaseModel): - args: Dict[str, Any] - hash: str # Mandatory hash field - # Show main page @app.get("/") async def read_index(): @@ -41,6 +39,51 @@ async def read_index(): html_content = f.read() return HTMLResponse(content=html_content, status_code=200) +# Pydantic model for request payload +class CommandModel(BaseModel): + args: Dict[str, Any] + hash: str # Mandatory hash field + +@app.get("/primary-ip", response_model=None) +def get_config(): + return get_primary_ip() + +@app.get("/frontend-config", response_model=None) +def get_config(hash: str): + + computed_hash = hashlib.sha256( ("frontend_config"+PASSWORD).encode("utf-8")).hexdigest() + + # Verify hash + if computed_hash != hash: + raise HTTPException(status_code=401, detail="Invalid hash") + + CONFIG_FILE = Path("config/frontend.yaml") + + # Checking the existence of the file + if not CONFIG_FILE.exists(): + raise HTTPException(status_code=404, detail="Файл frontend.yaml не найден") + + try: + # YAML reading and parsing + with open(CONFIG_FILE, "r", encoding="utf-8") as file: + data = yaml.safe_load(file) + + # Adding an id + for idx, item in enumerate(data): + item["id"] = idx + + return data + + except yaml.YAMLError as e: + # YAML parsing error + raise HTTPException(status_code=500, detail=f"Ошибка в формате YAML: {e}") + + except Exception as e: + # Any other error + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {e}") + + + # Process actions via POST @app.post("/action/{name:path}") async def handle_action(name: str, payload: CommandModel):