pré-rework
This commit is contained in:
1091
src-tauri/Cargo.lock
generated
1091
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,13 @@ opusic-sys = "0.6"
|
||||
tokio = { version = "1.50", features = ["full"] }
|
||||
reqwest = { version = "0.13", features = ["json", "rustls"] }
|
||||
thiserror = "2.0"
|
||||
tokio-tungstenite = { version = "0.29", features = ["rustls-tls-native-roots"] }
|
||||
futures-util = "0.3"
|
||||
url = "2.5"
|
||||
|
||||
figment = "0.10.19"
|
||||
parking_lot = "0.12"
|
||||
toml = "1.0.7+spec-1.1.0"
|
||||
ssh-key = { version = "0.7.0-rc.9", features = ["default", "crypto"] }
|
||||
base64 = "0.22"
|
||||
rand = "0.10"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod models;
|
||||
pub mod state;
|
||||
|
||||
pub use client::{ApiClient, ApiError, ApiResult};
|
||||
pub use state::ApiManager;
|
||||
@@ -1,35 +0,0 @@
|
||||
use crate::api::client::ApiClient;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Gère plusieurs instances de ApiClient (une par serveur).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ApiManager {
|
||||
clients: RwLock<HashMap<String, ApiClient>>,
|
||||
}
|
||||
|
||||
impl ApiManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
clients: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère ou crée un client pour une base_url donnée.
|
||||
pub fn get_or_create_client(&self, base_url: String) -> ApiClient {
|
||||
let mut clients = self.clients.write();
|
||||
if let Some(client) = clients.get(&base_url) {
|
||||
client.clone()
|
||||
} else {
|
||||
let client = ApiClient::new(base_url.clone());
|
||||
clients.insert(base_url, client.clone());
|
||||
client
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime un client.
|
||||
pub fn remove_client(&self, base_url: &str) {
|
||||
let mut clients = self.clients.write();
|
||||
clients.remove(base_url);
|
||||
}
|
||||
}
|
||||
135
src-tauri/src/api/web-client.ts
Normal file
135
src-tauri/src/api/web-client.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type {IApiClient} from './client';
|
||||
import type {
|
||||
CategoryResponse,
|
||||
ChannelResponse,
|
||||
ClaimAdminRequest,
|
||||
CreateCategoryRequest,
|
||||
CreateChannelRequest,
|
||||
CreateMessageRequest,
|
||||
CreateServerRequest,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
MessageResponse,
|
||||
ServerResponse,
|
||||
SshChallengeResponse,
|
||||
UserResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Implémentation Web (standard) de l'interface API.
|
||||
* Elle utilise fetch() pour communiquer directement avec le serveur.
|
||||
*/
|
||||
export class WebApiClient implements IApiClient {
|
||||
private token: string | null = null;
|
||||
|
||||
constructor(public readonly baseUrl: string) {
|
||||
this.token = localStorage.getItem(`token_${baseUrl}`);
|
||||
}
|
||||
|
||||
private async request<T>(method: string, path: string, body?: any): Promise<T> {
|
||||
// Nettoyer l'URL
|
||||
const cleanBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
const url = `${cleanBaseUrl}/${cleanPath}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorData;
|
||||
try {
|
||||
errorData = await response.json();
|
||||
} catch (e) {
|
||||
errorData = {message: await response.text()};
|
||||
}
|
||||
throw new Error(errorData.message || `Erreur HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private setToken(token: string) {
|
||||
this.token = token;
|
||||
localStorage.setItem(`token_${this.baseUrl}`, token);
|
||||
}
|
||||
|
||||
// AUTH
|
||||
async login(req: LoginRequest): Promise<LoginResponse> {
|
||||
const res = await this.request<LoginResponse>('POST', 'api/auth/login', req);
|
||||
this.setToken(res.token);
|
||||
return res;
|
||||
}
|
||||
|
||||
async verifyToken(): Promise<UserResponse> {
|
||||
return await this.request<UserResponse>('GET', 'api/auth/me');
|
||||
}
|
||||
|
||||
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
||||
return await this.request<void>('POST', 'api/auth/claim-admin', req);
|
||||
}
|
||||
|
||||
async sshChallenge(): Promise<SshChallengeResponse> {
|
||||
return await this.request<SshChallengeResponse>('POST', 'api/auth/ssh-challenge');
|
||||
}
|
||||
|
||||
// SERVER
|
||||
async getServerList(): Promise<ServerResponse[]> {
|
||||
return await this.request<ServerResponse[]>('GET', 'api/server');
|
||||
}
|
||||
|
||||
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
||||
return await this.request<ServerResponse>('POST', 'api/server', req);
|
||||
}
|
||||
|
||||
async getServerTree(serverId: string): Promise<any> {
|
||||
return await this.request<any>('GET', `api/server/servers/${serverId}/tree/`);
|
||||
}
|
||||
|
||||
// CATEGORY
|
||||
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
||||
const path = serverId ? `api/category?server_id=${serverId}` : 'api/category';
|
||||
return await this.request<CategoryResponse[]>('GET', path);
|
||||
}
|
||||
|
||||
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
||||
return await this.request<CategoryResponse>('POST', 'api/category', req);
|
||||
}
|
||||
|
||||
// CHANNEL
|
||||
async getChannelList(): Promise<ChannelResponse[]> {
|
||||
return await this.request<ChannelResponse[]>('GET', 'api/channel');
|
||||
}
|
||||
|
||||
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
||||
return await this.request<ChannelResponse>('POST', 'api/channel', req);
|
||||
}
|
||||
|
||||
// MESSAGE
|
||||
async getMessageList(): Promise<MessageResponse[]> {
|
||||
return await this.request<MessageResponse[]>('GET', 'api/message');
|
||||
}
|
||||
|
||||
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
||||
return await this.request<MessageResponse>('POST', 'api/message', req);
|
||||
}
|
||||
|
||||
// USER
|
||||
async getUserList(): Promise<UserResponse[]> {
|
||||
return await this.request<UserResponse[]>('GET', 'api/user');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::app::state::AppState;
|
||||
use crate::config::ConfigManager;
|
||||
use tauri::AppHandle;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
pub struct OxSpeakApp {
|
||||
tauri_handle: AppHandle,
|
||||
@@ -16,7 +17,33 @@ impl OxSpeakApp {
|
||||
|
||||
pub async fn run(&self) {
|
||||
println!("Starting OxSpeak backend...");
|
||||
// infinite loop
|
||||
println!("End of OxSpeak backend.");
|
||||
|
||||
let state = self.tauri_handle.state::<AppState>();
|
||||
let config = self.config.get_config();
|
||||
|
||||
// Les connexions seront initialisé dans la partie JS, pour garder la cross-compatibilité entre Tauri et Web
|
||||
// for server in config.servers {
|
||||
// println!("Initialisation du client pour {}", server.adresse);
|
||||
// let client = state
|
||||
// .client_manager
|
||||
// .add_client(server.adresse.clone(), server.token.clone());
|
||||
//
|
||||
// // Tentative de connexion WebSocket si un token est présent
|
||||
// if server.token.is_some() {
|
||||
// println!("Tentative de connexion WebSocket pour {}", server.adresse);
|
||||
// let handle_clone = self.tauri_handle.clone();
|
||||
// let server_addr = server.adresse.clone();
|
||||
// tokio::spawn(async move {
|
||||
// if let Err(e) = client.connect_ws(handle_clone).await {
|
||||
// eprintln!("Echec de connexion automatique pour {}: {}", server_addr, e);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// Boucle infinie pour garder le thread en vie
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::api::state::ApiManager;
|
||||
use crate::config::ConfigManager;
|
||||
use crate::network::ClientManager;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// État global de l'application.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AppState {
|
||||
pub api: ApiManager,
|
||||
pub config: OnceLock<ConfigManager>,
|
||||
pub client_manager: ClientManager,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
api: ApiManager::new(),
|
||||
config: OnceLock::new(),
|
||||
client_manager: ClientManager::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,14 @@ pub async fn config_update(
|
||||
new_config: ConfigTree,
|
||||
) -> Result<(), String> {
|
||||
let config = state.config.get().ok_or("Config non initialisée")?;
|
||||
println!("{:?}", new_config);
|
||||
config
|
||||
.update_config(new_config)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn generate_ssh_key() -> Result<String, String> {
|
||||
crate::config::config::generate_ssh_key_base64().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssh_key::PrivateKey;
|
||||
use ssh_key::{Algorithm, PrivateKey};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -25,6 +25,8 @@ pub struct ConfigTree {
|
||||
pub struct ConfigServer {
|
||||
pub adresse: String,
|
||||
pub identity: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
@@ -32,12 +34,14 @@ pub struct ConfigServer {
|
||||
pub enum IdentityMode {
|
||||
PrivateKeyPath,
|
||||
PrivateKeyBase64,
|
||||
Login,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct IdentityConfig {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
#[serde(default)]
|
||||
pub private_key: String,
|
||||
pub mode: IdentityMode,
|
||||
}
|
||||
@@ -47,7 +51,18 @@ impl ConfigManager {
|
||||
self.tree_config.lock().clone()
|
||||
}
|
||||
|
||||
pub async fn update_config(&self, new_config: ConfigTree) -> Result<(), std::io::Error> {
|
||||
pub async fn update_config(&self, mut new_config: ConfigTree) -> Result<(), std::io::Error> {
|
||||
// Auto-génération des clés si mode Base64 et champ vide
|
||||
for identity in &mut new_config.identities {
|
||||
if matches!(identity.mode, IdentityMode::PrivateKeyBase64)
|
||||
&& identity.private_key.trim().is_empty()
|
||||
{
|
||||
if let Ok(key) = generate_ssh_key_base64() {
|
||||
identity.private_key = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut lock = self.tree_config.lock();
|
||||
*lock = new_config.clone();
|
||||
@@ -67,7 +82,21 @@ impl ConfigManager {
|
||||
|
||||
if config_path.exists() && config_path.is_file() {
|
||||
let config_content = fs::read_to_string(config_path.clone()).await?;
|
||||
if let Ok(tree_config) = toml::from_str::<ConfigTree>(&config_content) {
|
||||
if let Ok(mut tree_config) = toml::from_str::<ConfigTree>(&config_content) {
|
||||
// Si aucune identité n'existe, on en crée une par défaut
|
||||
if tree_config.identities.is_empty() {
|
||||
let username = std::env::var("USER")
|
||||
.or_else(|_| std::env::var("USERNAME"))
|
||||
.unwrap_or_else(|_| "default".to_string());
|
||||
|
||||
tree_config.identities.push(IdentityConfig {
|
||||
id: "default".to_string(),
|
||||
username,
|
||||
private_key: String::new(),
|
||||
mode: IdentityMode::Login,
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(Self {
|
||||
tree_config: Arc::new(Mutex::new(tree_config)),
|
||||
path: config_path,
|
||||
@@ -82,9 +111,21 @@ impl ConfigManager {
|
||||
async fn create(config_dir: PathBuf, app_handle: AppHandle) -> Result<Self, std::io::Error> {
|
||||
let config_path = config_dir.join("config.toml");
|
||||
|
||||
// Obtenir le nom de l'utilisateur de session (cross-platform)
|
||||
let username = std::env::var("USER")
|
||||
.or_else(|_| std::env::var("USERNAME"))
|
||||
.unwrap_or_else(|_| "default".to_string());
|
||||
|
||||
let default_identity = IdentityConfig {
|
||||
id: "default".to_string(),
|
||||
username,
|
||||
private_key: String::new(),
|
||||
mode: IdentityMode::Login,
|
||||
};
|
||||
|
||||
let tree_config = ConfigTree {
|
||||
servers: Vec::new(),
|
||||
identities: Vec::new(),
|
||||
identities: vec![default_identity],
|
||||
};
|
||||
|
||||
let config = ConfigManager {
|
||||
@@ -137,6 +178,16 @@ impl IdentityConfig {
|
||||
PrivateKey::from_openssh(&key_content)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
IdentityMode::Login => {
|
||||
// Pas de clé privée pour ce mode
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
format!(
|
||||
"Le mode d'authentification {:?} n'utilise pas de clé privée.",
|
||||
self.mode
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +204,16 @@ impl ConfigServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_ssh_key_base64() -> Result<String, std::io::Error> {
|
||||
let key = PrivateKey::random(&mut rand::rng(), Algorithm::Ed25519)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
let openssh_content = key
|
||||
.to_openssh(ssh_key::LineEnding::LF)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
// On encode en base64 car c'est ce qu'attend le mode PrivateKeyBase64 actuel
|
||||
Ok(general_purpose::STANDARD.encode(openssh_content.as_bytes()))
|
||||
}
|
||||
|
||||
fn expand_path(path: &str) -> PathBuf {
|
||||
if path.starts_with("~/") {
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
|
||||
@@ -17,5 +17,5 @@ mod app;
|
||||
pub mod tauri_app;
|
||||
mod utils;
|
||||
|
||||
pub mod api;
|
||||
mod config;
|
||||
mod network;
|
||||
|
||||
131
src-tauri/src/network/client.rs
Normal file
131
src-tauri/src/network/client.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use crate::network::http::api::ApiClient;
|
||||
use crate::network::http::ws::client::WsClient;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tauri::AppHandle;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
pub base_url: String,
|
||||
pub api: ApiClient,
|
||||
pub ws_sender: Arc<RwLock<Option<mpsc::UnboundedSender<String>>>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(base_url: String, token: Option<String>) -> Self {
|
||||
let api = ApiClient::new(base_url.clone());
|
||||
if let Some(t) = token {
|
||||
api.set_token(t);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_url,
|
||||
api,
|
||||
ws_sender: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_ws(&self, app_handle: AppHandle) -> Result<(), String> {
|
||||
// 1. Vérifier le token via HTTP avant
|
||||
if let Some(_token) = self.api.get_token() {
|
||||
println!("Vérification du token pour {}", self.base_url);
|
||||
match self.api.verify_token().await {
|
||||
Ok(_) => {
|
||||
println!("Token valide pour {}", self.base_url);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Token invalide pour {}: {}", self.base_url, e);
|
||||
// Ici on pourrait déclencher un événement pour demander une ré-authentification
|
||||
// Pour le moment on s'arrête là ou on tente quand même la connexion WS
|
||||
// selon le besoin. Mais l'utilisateur dit "relance une authentification http"
|
||||
return Err("Token invalide, veuillez vous reconnecter".to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err("Aucun token disponible".to_string());
|
||||
}
|
||||
|
||||
let mut ws_sender_lock = self.ws_sender.write();
|
||||
|
||||
// Fermer l'ancienne connexion si elle existe
|
||||
if ws_sender_lock.is_some() {
|
||||
println!("Déconnexion de l'ancien WebSocket pour {}", self.base_url);
|
||||
*ws_sender_lock = None;
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let token = self.api.get_token();
|
||||
let client = WsClient::new(self.base_url.clone(), token, app_handle);
|
||||
|
||||
*ws_sender_lock = Some(tx);
|
||||
|
||||
// Lancer la tâche de connexion en arrière-plan
|
||||
tokio::spawn(async move {
|
||||
client.run(rx).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_ws(&self, message: String) -> Result<(), String> {
|
||||
let ws_sender_lock = self.ws_sender.read();
|
||||
if let Some(tx) = ws_sender_lock.as_ref() {
|
||||
tx.send(message)
|
||||
.map_err(|e| format!("Erreur d'envoi WebSocket: {}", e))
|
||||
} else {
|
||||
Err(format!(
|
||||
"Pas de connexion WebSocket active pour {}",
|
||||
self.base_url
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect_ws(&self) {
|
||||
let mut ws_sender_lock = self.ws_sender.write();
|
||||
*ws_sender_lock = None;
|
||||
println!("WebSocket déconnecté pour {}", self.base_url);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ClientManager {
|
||||
clients: RwLock<HashMap<String, Arc<Client>>>,
|
||||
}
|
||||
|
||||
impl ClientManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
clients: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_create_client(&self, base_url: String) -> Arc<Client> {
|
||||
let mut clients = self.clients.write();
|
||||
if let Some(client) = clients.get(&base_url) {
|
||||
client.clone()
|
||||
} else {
|
||||
let client = Arc::new(Client::new(base_url.clone(), None));
|
||||
clients.insert(base_url, client.clone());
|
||||
client
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_client(&self, base_url: String, token: Option<String>) -> Arc<Client> {
|
||||
let mut clients = self.clients.write();
|
||||
let client = Arc::new(Client::new(base_url.clone(), token));
|
||||
clients.insert(base_url, client.clone());
|
||||
client
|
||||
}
|
||||
|
||||
pub fn remove_client(&self, base_url: &str) {
|
||||
let mut clients = self.clients.write();
|
||||
clients.remove(base_url);
|
||||
}
|
||||
|
||||
pub fn get_client(&self, base_url: &str) -> Option<Arc<Client>> {
|
||||
let clients = self.clients.read();
|
||||
clients.get(base_url).cloned()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::api::models::*;
|
||||
use crate::network::http::api::models::*;
|
||||
use parking_lot::RwLock;
|
||||
use reqwest::{header, Client, Method, RequestBuilder, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -152,6 +152,10 @@ impl ApiClient {
|
||||
// --- ECOSYSTEMS ---
|
||||
|
||||
// Auth
|
||||
pub async fn verify_token(&self) -> ApiResult<UserResponse> {
|
||||
self.get("api/auth/me").await
|
||||
}
|
||||
|
||||
pub async fn login(&self, req: &LoginRequest) -> ApiResult<LoginResponse> {
|
||||
self.post("api/auth/login", req).await
|
||||
}
|
||||
@@ -173,6 +177,11 @@ impl ApiClient {
|
||||
self.post("api/server", req).await
|
||||
}
|
||||
|
||||
pub async fn server_tree(&self, server_id: &str) -> ApiResult<serde_json::Value> {
|
||||
self.get(&format!("api/server/servers/{}/tree/", server_id))
|
||||
.await
|
||||
}
|
||||
|
||||
// Category
|
||||
pub async fn category_list(&self, server_id: Option<&str>) -> ApiResult<Vec<CategoryResponse>> {
|
||||
let path = if let Some(id) = server_id {
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::models::*;
|
||||
use crate::api::ApiResult;
|
||||
use crate::app::state::AppState;
|
||||
use crate::network::http::api::client::ApiResult;
|
||||
use crate::network::http::api::models::*;
|
||||
use tauri::{command, State};
|
||||
|
||||
// --- AUTH ---
|
||||
@@ -11,20 +11,29 @@ pub async fn api_login(
|
||||
base_url: String,
|
||||
req: LoginRequest,
|
||||
) -> ApiResult<LoginResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
let res = client.login(&req).await?;
|
||||
client.set_token(res.token.clone());
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
let res = client.api.login(&req).await?;
|
||||
client.api.set_token(res.token.clone());
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_verify_token(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<UserResponse> {
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.verify_token().await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_claim_admin(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
req: ClaimAdminRequest,
|
||||
) -> ApiResult<()> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.claim_admin(&req).await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.claim_admin(&req).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -32,8 +41,8 @@ pub async fn api_ssh_challenge(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<SshChallengeResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.ssh_challenge().await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.ssh_challenge().await
|
||||
}
|
||||
|
||||
// --- SERVER ---
|
||||
@@ -43,8 +52,8 @@ pub async fn api_server_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<Vec<ServerResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.server_list().await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.server_list().await
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -53,8 +62,18 @@ pub async fn api_server_create(
|
||||
base_url: String,
|
||||
req: CreateServerRequest,
|
||||
) -> ApiResult<ServerResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.server_create(&req).await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.server_create(&req).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_server_tree(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
server_id: String,
|
||||
) -> ApiResult<serde_json::Value> {
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.server_tree(&server_id).await
|
||||
}
|
||||
|
||||
// --- CATEGORY ---
|
||||
@@ -65,8 +84,8 @@ pub async fn api_category_list(
|
||||
base_url: String,
|
||||
server_id: Option<String>,
|
||||
) -> ApiResult<Vec<CategoryResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.category_list(server_id.as_deref()).await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.category_list(server_id.as_deref()).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -75,8 +94,8 @@ pub async fn api_category_create(
|
||||
base_url: String,
|
||||
req: CreateCategoryRequest,
|
||||
) -> ApiResult<CategoryResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.category_create(&req).await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.category_create(&req).await
|
||||
}
|
||||
|
||||
// --- CHANNEL ---
|
||||
@@ -86,8 +105,8 @@ pub async fn api_channel_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<Vec<ChannelResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.channel_list().await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.channel_list().await
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -96,8 +115,8 @@ pub async fn api_channel_create(
|
||||
base_url: String,
|
||||
req: CreateChannelRequest,
|
||||
) -> ApiResult<ChannelResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.channel_create(&req).await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.channel_create(&req).await
|
||||
}
|
||||
|
||||
// --- MESSAGE ---
|
||||
@@ -107,8 +126,8 @@ pub async fn api_message_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<Vec<MessageResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.message_list().await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.message_list().await
|
||||
}
|
||||
|
||||
#[command]
|
||||
@@ -117,8 +136,8 @@ pub async fn api_message_create(
|
||||
base_url: String,
|
||||
req: CreateMessageRequest,
|
||||
) -> ApiResult<MessageResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.message_create(&req).await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.message_create(&req).await
|
||||
}
|
||||
|
||||
// --- USER ---
|
||||
@@ -128,6 +147,6 @@ pub async fn api_user_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<Vec<UserResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.user_list().await
|
||||
let client = state.client_manager.get_or_create_client(base_url);
|
||||
client.api.user_list().await
|
||||
}
|
||||
5
src-tauri/src/network/http/api/mod.rs
Normal file
5
src-tauri/src/network/http/api/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod models;
|
||||
|
||||
pub use client::ApiClient;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod api;
|
||||
pub mod ws;
|
||||
|
||||
106
src-tauri/src/network/http/ws/client.rs
Normal file
106
src-tauri/src/network/http/ws/client.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WsEvent {
|
||||
pub server_url: String,
|
||||
pub payload: serde_json::Value,
|
||||
}
|
||||
|
||||
pub struct WsClient {
|
||||
server_url: String,
|
||||
token: Option<String>,
|
||||
app_handle: AppHandle,
|
||||
}
|
||||
|
||||
impl WsClient {
|
||||
pub fn new(server_url: String, token: Option<String>, app_handle: AppHandle) -> Self {
|
||||
Self {
|
||||
server_url,
|
||||
token,
|
||||
app_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self, mut rx: mpsc::UnboundedReceiver<String>) {
|
||||
let ws_url = self.prepare_url();
|
||||
|
||||
loop {
|
||||
println!("Tentative de connexion WebSocket à {}", ws_url);
|
||||
|
||||
match connect_async(&ws_url).await {
|
||||
Ok((ws_stream, _)) => {
|
||||
println!("Connecté au WebSocket: {}", ws_url);
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
// Envoyer le token si présent pour l'auth initiale si nécessaire par le protocole
|
||||
// Ici on assume que le token est passé en query param ou via un message initial
|
||||
// Pour le moment, on se contente de la connexion.
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = read.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
let payload = serde_json::from_str::<serde_json::Value>(&text)
|
||||
.unwrap_or_else(|_| serde_json::Value::String(text.to_string()));
|
||||
|
||||
let _ = self.app_handle.emit("ws-message", WsEvent {
|
||||
server_url: self.server_url.clone(),
|
||||
payload,
|
||||
});
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => {
|
||||
println!("WebSocket fermé par le serveur");
|
||||
break;
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
eprintln!("Erreur WebSocket: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(to_send) = rx.recv() => {
|
||||
if let Err(e) = write.send(Message::Text(to_send.into())).await {
|
||||
eprintln!("Erreur lors de l'envoi WebSocket: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Erreur de connexion WebSocket: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnexion après un délai
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_url(&self) -> String {
|
||||
let mut url = self
|
||||
.server_url
|
||||
.replace("http://", "ws://")
|
||||
.replace("https://", "wss://");
|
||||
if !url.ends_with("/ws") {
|
||||
url = format!("{}/ws", url.trim_end_matches('/'));
|
||||
}
|
||||
|
||||
if let Some(token) = &self.token {
|
||||
if url.contains('?') {
|
||||
url = format!("{}&token={}", url, token);
|
||||
} else {
|
||||
url = format!("{}?token={}", url, token);
|
||||
}
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
33
src-tauri/src/network/http/ws/commands.rs
Normal file
33
src-tauri/src/network/http/ws/commands.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use crate::app::state::AppState;
|
||||
use tauri::{command, AppHandle, State};
|
||||
|
||||
#[command]
|
||||
pub async fn ws_connect(
|
||||
state: State<'_, AppState>,
|
||||
app_handle: AppHandle,
|
||||
server_url: String,
|
||||
) -> Result<(), String> {
|
||||
let client = state.client_manager.get_or_create_client(server_url);
|
||||
client.connect_ws(app_handle).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn ws_send(
|
||||
state: State<'_, AppState>,
|
||||
server_url: String,
|
||||
message: String,
|
||||
) -> Result<(), String> {
|
||||
if let Some(client) = state.client_manager.get_client(&server_url) {
|
||||
client.send_ws(message)
|
||||
} else {
|
||||
Err(format!("Client non trouvé pour {}", server_url))
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn ws_disconnect(state: State<'_, AppState>, server_url: String) -> Result<(), String> {
|
||||
if let Some(client) = state.client_manager.get_client(&server_url) {
|
||||
client.disconnect_ws();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
2
src-tauri/src/network/http/ws/mod.rs
Normal file
2
src-tauri/src/network/http/ws/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod client;
|
||||
pub mod http;
|
||||
pub mod udp;
|
||||
|
||||
pub use client::ClientManager;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::api::commands::*;
|
||||
use crate::app::ox_speak_app::OxSpeakApp;
|
||||
use crate::app::state::AppState;
|
||||
use crate::config::commands::*;
|
||||
use crate::config::ConfigManager;
|
||||
use crate::network::http::api::commands::*;
|
||||
use crate::network::http::ws::commands::*;
|
||||
use std::sync::Arc;
|
||||
use tauri::{Manager, WindowEvent};
|
||||
|
||||
@@ -23,10 +24,12 @@ pub async fn run() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
api_login,
|
||||
api_verify_token,
|
||||
api_claim_admin,
|
||||
api_ssh_challenge,
|
||||
api_server_list,
|
||||
api_server_create,
|
||||
api_server_tree,
|
||||
api_category_list,
|
||||
api_category_create,
|
||||
api_channel_list,
|
||||
@@ -36,6 +39,10 @@ pub async fn run() {
|
||||
api_user_list,
|
||||
config_get,
|
||||
config_update,
|
||||
generate_ssh_key,
|
||||
ws_connect,
|
||||
ws_send,
|
||||
ws_disconnect,
|
||||
])
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
||||
Reference in New Issue
Block a user