use crate::models::{ sanitize_filename, AppState, IncomingTextPayload, MediaItem, PeerNode, TextItem, UiSendTextRequest, UiTrustRequest, MEDIA_DIR, }; use axum::{ body::Bytes, extract::{Path, State}, http::{header, HeaderMap, StatusCode}, response::{Html, IntoResponse}, Json, }; use std::{path::PathBuf, sync::Arc}; /// Вшиваем внешний файл src/static/index.html в бинарник на этапе компиляции const INDEX_HTML: &str = include_str!("static/index.html"); pub async fn ui_index() -> Html<&'static str> { Html(INDEX_HTML) } pub async fn ui_get_state(State(state): State>) -> Json { let trusted_map = state.trusted_fingerprints.read().unwrap(); let mut peers_list: Vec = state .peers .read() .unwrap() .values() .cloned() .map(|mut p| { p.verified = trusted_map .get(&p.ip) .map(|fp| fp.eq_ignore_ascii_case(&p.fingerprint)) .unwrap_or(false); p }) .collect(); peers_list.sort_by(|a, b| a.name.cmp(&b.name)); let texts: Vec = state.ram_texts.read().unwrap().iter().cloned().collect(); let media: Vec = state.disk_media.read().unwrap().iter().cloned().collect(); Json(serde_json::json!({ "device_name": state.device_name, "my_ip": state.my_ip, "p2p_port": state.p2p_port, "my_fingerprint": state.my_fingerprint, "my_pin": state.my_fingerprint[..8].to_uppercase(), "peers": peers_list, "texts": texts, "media": media })) } pub async fn ui_trust_peer( State(state): State>, Json(req): Json, ) -> Result { let peers = state.peers.read().unwrap(); let peer = peers .get(&req.target_ip) .ok_or((StatusCode::NOT_FOUND, "Узел не найден".to_string()))?; let mut trusted = state.trusted_fingerprints.write().unwrap(); if req.trust { trusted.insert(peer.ip.clone(), peer.fingerprint.clone()); } else { trusted.remove(&peer.ip); } Ok(StatusCode::OK) } pub async fn ui_clear_ram(State(state): State>) -> StatusCode { state.ram_texts.write().unwrap().clear(); StatusCode::OK } pub async fn ui_send_text( State(state): State>, Json(req): Json, ) -> Result { let port = state .peers .read() .unwrap() .get(&req.target_ip) .map(|p| p.port) .unwrap_or(state.p2p_port); let url = format!("https://{}:{}/p2p/text", req.target_ip, port); let payload = IncomingTextPayload { sender: state.device_name.clone(), text: req.text, }; let resp = state .http_client .post(&url) .json(&payload) .send() .await .map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?; if resp.status().is_success() { Ok(StatusCode::OK) } else { Err((resp.status(), "Удаленный узел отклонил запрос".to_string())) } } pub async fn ui_send_media( State(state): State>, headers: HeaderMap, body: Bytes, ) -> Result { let target_ip = headers .get("X-Target-Ip") .and_then(|v| v.to_str().ok()) .ok_or((StatusCode::BAD_REQUEST, "Не указан IP получателя".to_string()))?; let filename = headers .get("X-Filename") .and_then(|v| v.to_str().ok()) .unwrap_or("file.bin"); let mime = headers .get(header::CONTENT_TYPE) .and_then(|v| v.to_str().ok()) .unwrap_or("application/octet-stream"); let port = state .peers .read() .unwrap() .get(target_ip) .map(|p| p.port) .unwrap_or(state.p2p_port); let url = format!("https://{}:{}/p2p/media", target_ip, port); let resp = state .http_client .post(&url) .header("X-Sender", &state.device_name) .header("X-Filename", filename) .header(header::CONTENT_TYPE, mime) .body(body) .send() .await .map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?; if resp.status().is_success() { Ok(StatusCode::OK) } else { Err((resp.status(), "Удаленный узел отклонил медиафайл".to_string())) } } pub async fn ui_serve_saved_file(Path(filename): Path) -> impl IntoResponse { let safe_name = sanitize_filename(&filename); let path = PathBuf::from(MEDIA_DIR).join(&safe_name); match tokio::fs::read(&path).await { Ok(bytes) => (StatusCode::OK, bytes).into_response(), Err(_) => (StatusCode::NOT_FOUND, "Файл не найден на диске").into_response(), } }