This commit is contained in:
2026-04-05 01:29:52 +02:00
parent aba2cfba7b
commit 5ac3847b5e
36 changed files with 677 additions and 1526 deletions
+289
View File
@@ -45,6 +45,17 @@ dependencies = [
"zeroize",
]
[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom 0.2.17",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -69,6 +80,23 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android_log-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
[[package]]
name = "android_logger"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3"
dependencies = [
"android_log-sys",
"env_filter",
"log",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -84,6 +112,12 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -335,6 +369,18 @@ dependencies = [
"serde_core",
]
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -376,6 +422,30 @@ dependencies = [
"piper",
]
[[package]]
name = "borsh"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
dependencies = [
"borsh-derive",
"bytes",
"cfg_aliases",
]
[[package]]
name = "borsh-derive"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
dependencies = [
"once_cell",
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "brotli"
version = "8.0.2"
@@ -403,6 +473,40 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "byte-unit"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d"
dependencies = [
"rust_decimal",
"schemars 1.2.1",
"serde",
"utf8-width",
]
[[package]]
name = "bytecheck"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
dependencies = [
"bytecheck_derive",
"ptr_meta",
"simdutf8",
]
[[package]]
name = "bytecheck_derive"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "bytemuck"
version = "1.25.0"
@@ -1228,6 +1332,16 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "env_filter"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
dependencies = [
"log",
"regex",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -1291,6 +1405,15 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "fern"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
dependencies = [
"log",
]
[[package]]
name = "fiat-crypto"
version = "0.3.0"
@@ -1395,6 +1518,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futf"
version = "0.1.5"
@@ -1840,6 +1969,9 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
@@ -2451,6 +2583,9 @@ name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
dependencies = [
"value-bag",
]
[[package]]
name = "lru-slab"
@@ -2648,6 +2783,15 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "objc2"
version = "0.6.4"
@@ -2827,6 +2971,7 @@ dependencies = [
"base64 0.22.1",
"figment",
"futures-util",
"log",
"opusic-sys",
"parking_lot",
"rand 0.10.0",
@@ -2836,6 +2981,7 @@ dependencies = [
"ssh-key",
"tauri",
"tauri-build",
"tauri-plugin-log",
"tauri-plugin-opener",
"thiserror 2.0.18",
"tokio",
@@ -3369,6 +3515,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "quick-xml"
version = "0.38.4"
@@ -3455,6 +3621,12 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.7.3"
@@ -3657,6 +3829,15 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rend"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
dependencies = [
"bytecheck",
]
[[package]]
name = "reqwest"
version = "0.13.2"
@@ -3724,6 +3905,35 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rkyv"
version = "0.7.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
dependencies = [
"bitvec",
"bytecheck",
"bytes",
"hashbrown 0.12.3",
"ptr_meta",
"rend",
"rkyv_derive",
"seahash",
"tinyvec",
"uuid",
]
[[package]]
name = "rkyv_derive"
version = "0.7.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "rsa"
version = "0.10.0-rc.17"
@@ -3740,6 +3950,22 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust_decimal"
version = "1.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0"
dependencies = [
"arrayvec",
"borsh",
"bytes",
"num-traits",
"rand 0.8.5",
"rkyv",
"serde",
"serde_json",
]
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -3945,6 +4171,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "sec1"
version = "0.8.0"
@@ -4271,6 +4503,12 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -4607,6 +4845,12 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -4744,6 +4988,28 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-log"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93"
dependencies = [
"android_logger",
"byte-unit",
"fern",
"log",
"objc2",
"objc2-foundation",
"serde",
"serde_json",
"serde_repr",
"swift-rs",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
"time",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.3"
@@ -4948,7 +5214,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde_core",
"time-core",
@@ -5443,6 +5711,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -5461,6 +5735,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "value-bag"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
[[package]]
name = "version-compare"
version = "0.2.1"
@@ -6389,6 +6669,15 @@ dependencies = [
"x11-dl",
]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x11"
version = "2.21.0"
+2
View File
@@ -36,3 +36,5 @@ 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"
tauri-plugin-log = "2"
log = "0.4"
-1
View File
@@ -1,4 +1,3 @@
fn main() {
tauri_build::build()
}
+6 -3
View File
@@ -2,9 +2,12 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"core:default",
"opener:default"
"opener:default",
"log:default"
]
}
}
-135
View File
@@ -1,135 +0,0 @@
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');
}
}
-3
View File
@@ -1,19 +1,16 @@
use crate::config::ConfigManager;
use crate::network::ClientManager;
use std::sync::OnceLock;
/// État global de l'application.
#[derive(Debug, Default)]
pub struct AppState {
pub config: OnceLock<ConfigManager>,
pub client_manager: ClientManager,
}
impl AppState {
pub fn new() -> Self {
Self {
config: OnceLock::new(),
client_manager: ClientManager::new(),
}
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod commands;
pub mod input;
pub mod output;
+66 -1
View File
@@ -14,13 +14,78 @@ 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 save_token(
state: State<'_, AppState>,
base_url: String,
token: String,
) -> Result<(), String> {
let config_manager = state.config.get().ok_or("Config non initialisée")?;
let mut config = config_manager.get_config();
// Find server by address or add it
let found = config.servers.iter_mut().any(|s| {
if s.adresse == base_url {
s.token = Some(token.clone());
true
} else {
false
}
});
if !found {
config.servers.push(crate::config::config::ConfigServer {
adresse: base_url,
identity: "default".to_string(),
token: Some(token),
});
}
config_manager
.update_config(config)
.await
.map_err(|e| e.to_string())
}
#[command]
pub async fn load_token(
state: State<'_, AppState>,
base_url: String,
) -> Result<Option<String>, String> {
let config_manager = state.config.get().ok_or("Config non initialisée")?;
let config = config_manager.get_config();
let token = config
.servers
.iter()
.find(|s| s.adresse == base_url)
.and_then(|s| s.token.clone());
Ok(token)
}
#[command]
pub async fn clear_token(state: State<'_, AppState>, base_url: String) -> Result<(), String> {
let config_manager = state.config.get().ok_or("Config non initialisée")?;
let mut config = config_manager.get_config();
if let Some(server) = config.servers.iter_mut().find(|s| s.adresse == base_url) {
server.token = None;
config_manager
.update_config(config)
.await
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[command]
pub async fn generate_ssh_key() -> Result<String, String> {
crate::config::config::generate_ssh_key_base64().map_err(|e| e.to_string())
+1 -1
View File
@@ -12,7 +12,7 @@ async fn main() {
// si tu remarques que certains utilisateurs ont encore des pages blanches.
// std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
}
// ox_speak_lib::run()
ox_speak_lib::tauri_app::run().await;
}
-131
View File
@@ -1,131 +0,0 @@
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()
}
}
-256
View File
@@ -1,256 +0,0 @@
use crate::network::http::api::models::*;
use parking_lot::RwLock;
use reqwest::{header, Client, Method, RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::sync::Arc;
/// Erreurs possibles lors de l'utilisation du client API.
#[derive(Debug, thiserror::Error, Serialize)]
#[serde(tag = "type", content = "data")]
pub enum ApiError {
#[error("Erreur de requête HTTP: {0}")]
Http(String),
#[error("Erreur de désérialisation: {0}")]
Json(String),
#[error("Réponse d'erreur du serveur: {status} - {body}")]
ServerError { status: u16, body: String },
}
impl From<reqwest::Error> for ApiError {
fn from(err: reqwest::Error) -> Self {
ApiError::Http(err.to_string())
}
}
impl From<serde_json::Error> for ApiError {
fn from(err: serde_json::Error) -> Self {
ApiError::Json(err.to_string())
}
}
pub type ApiResult<T> = Result<T, ApiError>;
/// Un client API générique capable de se connecter à plusieurs serveurs.
/// Il gère l'authentification par token JWT de manière thread-safe.
#[derive(Clone, Debug)]
pub struct ApiClient {
base_url: String,
inner: Client,
token: Arc<RwLock<Option<String>>>,
}
impl ApiClient {
/// Initialise un nouveau client pour un serveur spécifique.
pub fn new(base_url: String) -> Self {
let mut headers = header::HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
let inner = Client::builder()
.default_headers(headers)
.build()
.unwrap_or_else(|_| Client::new());
Self {
base_url: base_url.trim_end_matches('/').to_string(),
inner,
token: Arc::new(RwLock::new(None)),
}
}
/// Définit le token JWT à utiliser pour les requêtes authentifiées.
pub fn set_token(&self, token: String) {
let mut lock = self.token.write();
*lock = Some(token);
}
/// Récupère le token JWT actuel s'il existe.
pub fn get_token(&self) -> Option<String> {
self.token.read().clone()
}
/// Supprime le token JWT actuel.
pub fn clear_token(&self) {
let mut lock = self.token.write();
*lock = None;
}
/// Construit une requête HTTP avec l'authentification si disponible.
fn build_request(&self, method: Method, path: &str) -> RequestBuilder {
let url = if path.starts_with("http") {
path.to_string()
} else {
format!("{}/{}", self.base_url, path.trim_start_matches('/'))
};
let mut rb = self.inner.request(method, url);
if let Some(token) = self.token.read().as_ref() {
rb = rb.header(header::AUTHORIZATION, format!("Bearer {}", token));
}
rb
}
async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> ApiResult<T> {
let status = response.status();
if status.is_success() {
let data = response.json::<T>().await?;
Ok(data)
} else {
let body = response.text().await.unwrap_or_default();
Err(ApiError::ServerError {
status: status.as_u16(),
body,
})
}
}
pub async fn get<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
let response = self.build_request(Method::GET, path).send().await?;
self.handle_response(response).await
}
pub async fn post<B: Serialize, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> ApiResult<T> {
let response = self
.build_request(Method::POST, path)
.json(body)
.send()
.await?;
self.handle_response(response).await
}
pub async fn post_empty<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
let response = self.build_request(Method::POST, path).send().await?;
self.handle_response(response).await
}
pub async fn put<B: Serialize, T: DeserializeOwned>(
&self,
path: &str,
body: &B,
) -> ApiResult<T> {
let response = self
.build_request(Method::PUT, path)
.json(body)
.send()
.await?;
self.handle_response(response).await
}
pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
let response = self.build_request(Method::DELETE, path).send().await?;
self.handle_response(response).await
}
// --- 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
}
pub async fn claim_admin(&self, req: &ClaimAdminRequest) -> ApiResult<()> {
self.post("api/auth/claim-admin", req).await
}
pub async fn ssh_challenge(&self) -> ApiResult<SshChallengeResponse> {
self.post_empty("api/auth/ssh-challenge").await
}
// Server
pub async fn server_list(&self) -> ApiResult<Vec<ServerResponse>> {
self.get("api/server").await
}
pub async fn server_create(&self, req: &CreateServerRequest) -> ApiResult<ServerResponse> {
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 {
format!("api/category?server_id={}", id)
} else {
"api/category".to_string()
};
self.get(&path).await
}
pub async fn category_create(
&self,
req: &CreateCategoryRequest,
) -> ApiResult<CategoryResponse> {
self.post("api/category", req).await
}
pub async fn category_detail(&self, id: &str) -> ApiResult<CategoryResponse> {
self.get(&format!("api/category/{}", id)).await
}
pub async fn category_update(
&self,
id: &str,
req: &CreateCategoryRequest,
) -> ApiResult<CategoryResponse> {
self.put(&format!("api/category/{}", id), req).await
}
pub async fn category_delete(&self, id: &str) -> ApiResult<()> {
self.delete(&format!("api/category/{}", id)).await
}
// Channel
pub async fn channel_list(&self) -> ApiResult<Vec<ChannelResponse>> {
self.get("api/channel").await
}
pub async fn channel_create(&self, req: &CreateChannelRequest) -> ApiResult<ChannelResponse> {
self.post("api/channel", req).await
}
pub async fn channel_detail(&self, id: &str) -> ApiResult<ChannelResponse> {
self.get(&format!("api/channel/{}", id)).await
}
pub async fn channel_update(
&self,
id: &str,
req: &CreateChannelRequest,
) -> ApiResult<ChannelResponse> {
self.put(&format!("api/channel/{}", id), req).await
}
pub async fn channel_delete(&self, id: &str) -> ApiResult<()> {
self.delete(&format!("api/channel/{}", id)).await
}
// Message
pub async fn message_list(&self) -> ApiResult<Vec<MessageResponse>> {
self.get("api/message").await
}
pub async fn message_create(&self, req: &CreateMessageRequest) -> ApiResult<MessageResponse> {
self.post("api/message", req).await
}
// User
pub async fn user_list(&self) -> ApiResult<Vec<UserResponse>> {
self.get("api/user").await
}
}
-152
View File
@@ -1,152 +0,0 @@
use crate::app::state::AppState;
use crate::network::http::api::client::ApiResult;
use crate::network::http::api::models::*;
use tauri::{command, State};
// --- AUTH ---
#[command]
pub async fn api_login(
state: State<'_, AppState>,
base_url: String,
req: LoginRequest,
) -> ApiResult<LoginResponse> {
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.client_manager.get_or_create_client(base_url);
client.api.claim_admin(&req).await
}
#[command]
pub async fn api_ssh_challenge(
state: State<'_, AppState>,
base_url: String,
) -> ApiResult<SshChallengeResponse> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.ssh_challenge().await
}
// --- SERVER ---
#[command]
pub async fn api_server_list(
state: State<'_, AppState>,
base_url: String,
) -> ApiResult<Vec<ServerResponse>> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.server_list().await
}
#[command]
pub async fn api_server_create(
state: State<'_, AppState>,
base_url: String,
req: CreateServerRequest,
) -> ApiResult<ServerResponse> {
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 ---
#[command]
pub async fn api_category_list(
state: State<'_, AppState>,
base_url: String,
server_id: Option<String>,
) -> ApiResult<Vec<CategoryResponse>> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.category_list(server_id.as_deref()).await
}
#[command]
pub async fn api_category_create(
state: State<'_, AppState>,
base_url: String,
req: CreateCategoryRequest,
) -> ApiResult<CategoryResponse> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.category_create(&req).await
}
// --- CHANNEL ---
#[command]
pub async fn api_channel_list(
state: State<'_, AppState>,
base_url: String,
) -> ApiResult<Vec<ChannelResponse>> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.channel_list().await
}
#[command]
pub async fn api_channel_create(
state: State<'_, AppState>,
base_url: String,
req: CreateChannelRequest,
) -> ApiResult<ChannelResponse> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.channel_create(&req).await
}
// --- MESSAGE ---
#[command]
pub async fn api_message_list(
state: State<'_, AppState>,
base_url: String,
) -> ApiResult<Vec<MessageResponse>> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.message_list().await
}
#[command]
pub async fn api_message_create(
state: State<'_, AppState>,
base_url: String,
req: CreateMessageRequest,
) -> ApiResult<MessageResponse> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.message_create(&req).await
}
// --- USER ---
#[command]
pub async fn api_user_list(
state: State<'_, AppState>,
base_url: String,
) -> ApiResult<Vec<UserResponse>> {
let client = state.client_manager.get_or_create_client(base_url);
client.api.user_list().await
}
-5
View File
@@ -1,5 +0,0 @@
pub mod client;
pub mod commands;
pub mod models;
pub use client::ApiClient;
-115
View File
@@ -1,115 +0,0 @@
use serde::{Deserialize, Serialize};
// --- AUTH ---
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoginRequest {
pub username: String,
pub password: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoginResponse {
pub token: String,
pub username: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ClaimAdminRequest {
pub token: String,
pub username: String,
pub password: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SshChallengeResponse {
pub challenge: String,
}
// --- SERVER ---
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateServerRequest {
pub name: String,
pub password: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ServerResponse {
pub id: String,
pub name: String,
pub created_at: String,
pub updated_at: String,
}
// --- CATEGORY ---
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateCategoryRequest {
pub server_id: String,
pub name: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CategoryResponse {
pub id: String,
pub name: String,
}
// --- CHANNEL ---
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ChannelType {
Text,
Voice,
DM,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateChannelRequest {
pub channel_type: ChannelType,
pub category_id: Option<String>,
pub name: Option<String>,
pub position: Option<i32>,
pub server_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChannelResponse {
pub id: String,
pub name: Option<String>,
pub position: i32,
pub channel_type: ChannelType,
pub category_id: Option<String>,
}
// --- MESSAGE ---
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateMessageRequest {
pub channel_id: String,
pub content: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MessageResponse {
pub id: String,
pub channel_id: String,
pub author_id: String,
pub content: String,
}
// --- USER ---
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CreateUserRequest {
pub username: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserResponse {
pub id: String,
pub username: String,
pub pub_key: String,
}
-2
View File
@@ -1,2 +0,0 @@
pub mod api;
pub mod ws;
-106
View File
@@ -1,106 +0,0 @@
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
View File
@@ -1,33 +0,0 @@
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
View File
@@ -1,2 +0,0 @@
pub mod client;
pub mod commands;
-4
View File
@@ -1,5 +1 @@
pub mod client;
pub mod http;
pub mod udp;
pub use client::ClientManager;
+25 -19
View File
@@ -2,10 +2,10 @@ 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 log::info;
use std::sync::Arc;
use tauri::{Manager, WindowEvent};
use tauri_plugin_log::{Target, TargetKind};
// Séparation du generate_context, sinon l'RustRover (l'ide) rame énormément dans la fonction run
fn get_tauri_context() -> tauri::Context<tauri::Wry> {
@@ -20,31 +20,37 @@ pub async fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(
tauri_plugin_log::Builder::new()
.targets([
Target::new(TargetKind::Stdout),
Target::new(TargetKind::LogDir { file_name: None }),
Target::new(TargetKind::Webview),
])
.build(),
)
// .plugin(
// tauri_plugin_log::Builder::new()
// .targets([
// Target::new(TargetKind::Stdout),
// Target::new(TargetKind::LogDir { file_name: None }),
// Target::new(TargetKind::Webview),
// ])
// .level(log::LevelFilter::Trace)
// .build(),
// )
.manage(AppState::new())
.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,
api_channel_create,
api_message_list,
api_message_create,
api_user_list,
config_get,
config_update,
generate_ssh_key,
ws_connect,
ws_send,
ws_disconnect,
save_token,
load_token,
clear_token,
])
.setup(|app| {
info!("--- Tauri App Initialized (Rust side) ---");
let handle = app.handle().clone();
// Démarrer le backend
+1
View File
@@ -12,6 +12,7 @@
"app": {
"windows": [
{
"label": "main",
"title": "ox-speak",
"width": 1024,
"height": 768