diff --git a/package.json b/package.json index 862e2ce..1c697de 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@nuxt/ui": "^4.5.1", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-log": "~2", "@tauri-apps/plugin-opener": "^2", "pinia": "^3.0.4", "tailwindcss": "^4.2.2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 03f69da..38eafae 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d12256d..659ff49 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 55335e6..d860e1e 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,4 +1,3 @@ - fn main() { tauri_build::build() } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4cdbf49..bf2c212 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -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" ] -} +} \ No newline at end of file diff --git a/src-tauri/src/api/web-client.ts b/src-tauri/src/api/web-client.ts deleted file mode 100644 index 2073952..0000000 --- a/src-tauri/src/api/web-client.ts +++ /dev/null @@ -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(method: string, path: string, body?: any): Promise { - // 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 = { - '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 { - const res = await this.request('POST', 'api/auth/login', req); - this.setToken(res.token); - return res; - } - - async verifyToken(): Promise { - return await this.request('GET', 'api/auth/me'); - } - - async claimAdmin(req: ClaimAdminRequest): Promise { - return await this.request('POST', 'api/auth/claim-admin', req); - } - - async sshChallenge(): Promise { - return await this.request('POST', 'api/auth/ssh-challenge'); - } - - // SERVER - async getServerList(): Promise { - return await this.request('GET', 'api/server'); - } - - async createServer(req: CreateServerRequest): Promise { - return await this.request('POST', 'api/server', req); - } - - async getServerTree(serverId: string): Promise { - return await this.request('GET', `api/server/servers/${serverId}/tree/`); - } - - // CATEGORY - async getCategoryList(serverId?: string): Promise { - const path = serverId ? `api/category?server_id=${serverId}` : 'api/category'; - return await this.request('GET', path); - } - - async createCategory(req: CreateCategoryRequest): Promise { - return await this.request('POST', 'api/category', req); - } - - // CHANNEL - async getChannelList(): Promise { - return await this.request('GET', 'api/channel'); - } - - async createChannel(req: CreateChannelRequest): Promise { - return await this.request('POST', 'api/channel', req); - } - - // MESSAGE - async getMessageList(): Promise { - return await this.request('GET', 'api/message'); - } - - async createMessage(req: CreateMessageRequest): Promise { - return await this.request('POST', 'api/message', req); - } - - // USER - async getUserList(): Promise { - return await this.request('GET', 'api/user'); - } -} diff --git a/src-tauri/src/app/state.rs b/src-tauri/src/app/state.rs index fefb9a9..cbb6b65 100644 --- a/src-tauri/src/app/state.rs +++ b/src-tauri/src/app/state.rs @@ -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, - pub client_manager: ClientManager, } impl AppState { pub fn new() -> Self { Self { config: OnceLock::new(), - client_manager: ClientManager::new(), } } } diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index e69de29..2c92805 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -0,0 +1,3 @@ +pub mod commands; +pub mod input; +pub mod output; diff --git a/src-tauri/src/config/commands.rs b/src-tauri/src/config/commands.rs index 787afa6..e93ae3d 100644 --- a/src-tauri/src/config/commands.rs +++ b/src-tauri/src/config/commands.rs @@ -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, 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 { crate::config::config::generate_ssh_key_base64().map_err(|e| e.to_string()) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b29e705..2be979b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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; } diff --git a/src-tauri/src/network/client.rs b/src-tauri/src/network/client.rs deleted file mode 100644 index a505c14..0000000 --- a/src-tauri/src/network/client.rs +++ /dev/null @@ -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>>>, -} - -impl Client { - pub fn new(base_url: String, token: Option) -> 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>>, -} - -impl ClientManager { - pub fn new() -> Self { - Self { - clients: RwLock::new(HashMap::new()), - } - } - - pub fn get_or_create_client(&self, base_url: String) -> Arc { - 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) -> Arc { - 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> { - let clients = self.clients.read(); - clients.get(base_url).cloned() - } -} diff --git a/src-tauri/src/network/http/api/client.rs b/src-tauri/src/network/http/api/client.rs deleted file mode 100644 index 051eb33..0000000 --- a/src-tauri/src/network/http/api/client.rs +++ /dev/null @@ -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 for ApiError { - fn from(err: reqwest::Error) -> Self { - ApiError::Http(err.to_string()) - } -} - -impl From for ApiError { - fn from(err: serde_json::Error) -> Self { - ApiError::Json(err.to_string()) - } -} - -pub type ApiResult = Result; - -/// 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>>, -} - -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 { - 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(&self, response: Response) -> ApiResult { - let status = response.status(); - if status.is_success() { - let data = response.json::().await?; - Ok(data) - } else { - let body = response.text().await.unwrap_or_default(); - Err(ApiError::ServerError { - status: status.as_u16(), - body, - }) - } - } - - pub async fn get(&self, path: &str) -> ApiResult { - let response = self.build_request(Method::GET, path).send().await?; - self.handle_response(response).await - } - - pub async fn post( - &self, - path: &str, - body: &B, - ) -> ApiResult { - let response = self - .build_request(Method::POST, path) - .json(body) - .send() - .await?; - self.handle_response(response).await - } - - pub async fn post_empty(&self, path: &str) -> ApiResult { - let response = self.build_request(Method::POST, path).send().await?; - self.handle_response(response).await - } - - pub async fn put( - &self, - path: &str, - body: &B, - ) -> ApiResult { - let response = self - .build_request(Method::PUT, path) - .json(body) - .send() - .await?; - self.handle_response(response).await - } - - pub async fn delete(&self, path: &str) -> ApiResult { - let response = self.build_request(Method::DELETE, path).send().await?; - self.handle_response(response).await - } - - // --- ECOSYSTEMS --- - - // Auth - pub async fn verify_token(&self) -> ApiResult { - self.get("api/auth/me").await - } - - pub async fn login(&self, req: &LoginRequest) -> ApiResult { - 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 { - self.post_empty("api/auth/ssh-challenge").await - } - - // Server - pub async fn server_list(&self) -> ApiResult> { - self.get("api/server").await - } - - pub async fn server_create(&self, req: &CreateServerRequest) -> ApiResult { - self.post("api/server", req).await - } - - pub async fn server_tree(&self, server_id: &str) -> ApiResult { - self.get(&format!("api/server/servers/{}/tree/", server_id)) - .await - } - - // Category - pub async fn category_list(&self, server_id: Option<&str>) -> ApiResult> { - 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 { - self.post("api/category", req).await - } - - pub async fn category_detail(&self, id: &str) -> ApiResult { - self.get(&format!("api/category/{}", id)).await - } - - pub async fn category_update( - &self, - id: &str, - req: &CreateCategoryRequest, - ) -> ApiResult { - 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> { - self.get("api/channel").await - } - - pub async fn channel_create(&self, req: &CreateChannelRequest) -> ApiResult { - self.post("api/channel", req).await - } - - pub async fn channel_detail(&self, id: &str) -> ApiResult { - self.get(&format!("api/channel/{}", id)).await - } - - pub async fn channel_update( - &self, - id: &str, - req: &CreateChannelRequest, - ) -> ApiResult { - 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> { - self.get("api/message").await - } - - pub async fn message_create(&self, req: &CreateMessageRequest) -> ApiResult { - self.post("api/message", req).await - } - - // User - pub async fn user_list(&self) -> ApiResult> { - self.get("api/user").await - } -} diff --git a/src-tauri/src/network/http/api/commands.rs b/src-tauri/src/network/http/api/commands.rs deleted file mode 100644 index 8808243..0000000 --- a/src-tauri/src/network/http/api/commands.rs +++ /dev/null @@ -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 { - 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 { - 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 { - 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> { - 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 { - 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 { - 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, -) -> ApiResult> { - 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 { - 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> { - 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 { - 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> { - 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 { - 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> { - let client = state.client_manager.get_or_create_client(base_url); - client.api.user_list().await -} diff --git a/src-tauri/src/network/http/api/mod.rs b/src-tauri/src/network/http/api/mod.rs deleted file mode 100644 index e0b39e4..0000000 --- a/src-tauri/src/network/http/api/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod client; -pub mod commands; -pub mod models; - -pub use client::ApiClient; diff --git a/src-tauri/src/network/http/api/models.rs b/src-tauri/src/network/http/api/models.rs deleted file mode 100644 index b60894f..0000000 --- a/src-tauri/src/network/http/api/models.rs +++ /dev/null @@ -1,115 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// --- AUTH --- - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct LoginRequest { - pub username: String, - pub password: Option, -} - -#[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, -} - -#[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, -} - -#[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, - pub name: Option, - pub position: Option, - pub server_id: Option, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ChannelResponse { - pub id: String, - pub name: Option, - pub position: i32, - pub channel_type: ChannelType, - pub category_id: Option, -} - -// --- 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, -} diff --git a/src-tauri/src/network/http/mod.rs b/src-tauri/src/network/http/mod.rs deleted file mode 100644 index b3bd2fc..0000000 --- a/src-tauri/src/network/http/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod api; -pub mod ws; diff --git a/src-tauri/src/network/http/ws/client.rs b/src-tauri/src/network/http/ws/client.rs deleted file mode 100644 index db637f0..0000000 --- a/src-tauri/src/network/http/ws/client.rs +++ /dev/null @@ -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, - app_handle: AppHandle, -} - -impl WsClient { - pub fn new(server_url: String, token: Option, app_handle: AppHandle) -> Self { - Self { - server_url, - token, - app_handle, - } - } - - pub async fn run(self, mut rx: mpsc::UnboundedReceiver) { - 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::(&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 - } -} diff --git a/src-tauri/src/network/http/ws/commands.rs b/src-tauri/src/network/http/ws/commands.rs deleted file mode 100644 index a8c38b6..0000000 --- a/src-tauri/src/network/http/ws/commands.rs +++ /dev/null @@ -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(()) -} diff --git a/src-tauri/src/network/http/ws/mod.rs b/src-tauri/src/network/http/ws/mod.rs deleted file mode 100644 index 7440cad..0000000 --- a/src-tauri/src/network/http/ws/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod client; -pub mod commands; diff --git a/src-tauri/src/network/mod.rs b/src-tauri/src/network/mod.rs index e455523..7e5aaa1 100644 --- a/src-tauri/src/network/mod.rs +++ b/src-tauri/src/network/mod.rs @@ -1,5 +1 @@ -pub mod client; -pub mod http; pub mod udp; - -pub use client::ClientManager; diff --git a/src-tauri/src/tauri_app.rs b/src-tauri/src/tauri_app.rs index 95f446f..c803d87 100644 --- a/src-tauri/src/tauri_app.rs +++ b/src-tauri/src/tauri_app.rs @@ -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 { @@ -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 diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b1d7f7f..de644f9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,6 +12,7 @@ "app": { "windows": [ { + "label": "main", "title": "ox-speak", "width": 1024, "height": 768 diff --git a/src/App.vue b/src/App.vue index 16092a7..c1abebe 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,11 +2,6 @@ import {onMounted, onUnmounted, ref} from 'vue'; import {NavigationMenuItem} from "@nuxt/ui"; import {createApiClient} from "./api"; -import {useConfigStore, useServerStore} from "./stores"; - -// stores -const configStore = useConfigStore(); -const serverStore = useServerStore(); // vars let unlisten: (() => void) | null = null; @@ -48,9 +43,6 @@ async function fetchServers() { // ]) onMounted(async () => { - // config - await configStore.init(); - // fetch servers await fetchServers() }) diff --git a/src/api/index.ts b/src/api/index.ts index ad4e487..5c5e4d8 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,19 +1,14 @@ export * from './types'; export * from './client'; -export * from './tauri-client'; -export * from './web-client'; +export * from './api-client'; -import {TauriApiClient} from './tauri-client'; -import {WebApiClient} from './web-client'; +import {ApiClient} from './api-client'; import type {IApiClient} from './client'; /** - * Usine pour créer des clients API. - * Elle retourne une implémentation Web ou Tauri selon l'environnement. + * Factory for creating API clients. + * Always returns a unified ApiClient that works in both Web and Tauri. */ export function createApiClient(baseUrl: string): IApiClient { - if ((window as any).__TAURI_INTERNALS__) { - return new TauriApiClient(baseUrl); - } - return new WebApiClient(baseUrl); + return new ApiClient(baseUrl); } diff --git a/src/api/tauri-client.ts b/src/api/tauri-client.ts deleted file mode 100644 index a0a0392..0000000 --- a/src/api/tauri-client.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {invoke} from '@tauri-apps/api/core'; -import type {IApiClient} from './client'; -import type { - CategoryResponse, - ChannelResponse, - ClaimAdminRequest, - CreateCategoryRequest, - CreateChannelRequest, - CreateMessageRequest, - CreateServerRequest, - LoginRequest, - LoginResponse, - MessageResponse, - ServerResponse, - SshChallengeResponse, - UserResponse, -} from './types'; - -/** - * Implémentation Tauri de l'interface API. - * Cette version utilise 'invoke' pour communiquer avec le backend Rust. - */ -export class TauriApiClient implements IApiClient { - constructor(public readonly baseUrl: string) { - } - - // AUTH - async login(req: LoginRequest): Promise { - return await invoke('api_login', {baseUrl: this.baseUrl, req}); - } - - async verifyToken(): Promise { - return await invoke('api_verify_token', {baseUrl: this.baseUrl}); - } - - async claimAdmin(req: ClaimAdminRequest): Promise { - return await invoke('api_claim_admin', {baseUrl: this.baseUrl, req}); - } - - async sshChallenge(): Promise { - return await invoke('api_ssh_challenge', {baseUrl: this.baseUrl}); - } - - // SERVER - async getServerList(): Promise { - return await invoke('api_server_list', {baseUrl: this.baseUrl}); - } - - async createServer(req: CreateServerRequest): Promise { - return await invoke('api_server_create', {baseUrl: this.baseUrl, req}); - } - - async getServerTree(serverId: string): Promise { - return await invoke('api_server_tree', {baseUrl: this.baseUrl, serverId}); - } - - // CATEGORY - async getCategoryList(serverId?: string): Promise { - return await invoke('api_category_list', { - baseUrl: this.baseUrl, - serverId: serverId ?? null - }); - } - - async createCategory(req: CreateCategoryRequest): Promise { - return await invoke('api_category_create', {baseUrl: this.baseUrl, req}); - } - - // CHANNEL - async getChannelList(): Promise { - return await invoke('api_channel_list', {baseUrl: this.baseUrl}); - } - - async createChannel(req: CreateChannelRequest): Promise { - return await invoke('api_channel_create', {baseUrl: this.baseUrl, req}); - } - - // MESSAGE - async getMessageList(): Promise { - return await invoke('api_message_list', {baseUrl: this.baseUrl}); - } - - async createMessage(req: CreateMessageRequest): Promise { - return await invoke('api_message_create', {baseUrl: this.baseUrl, req}); - } - - // USER - async getUserList(): Promise { - return await invoke('api_user_list', {baseUrl: this.baseUrl}); - } -} diff --git a/src/api/web-client.ts b/src/api/web-client.ts deleted file mode 100644 index 2073952..0000000 --- a/src/api/web-client.ts +++ /dev/null @@ -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(method: string, path: string, body?: any): Promise { - // 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 = { - '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 { - const res = await this.request('POST', 'api/auth/login', req); - this.setToken(res.token); - return res; - } - - async verifyToken(): Promise { - return await this.request('GET', 'api/auth/me'); - } - - async claimAdmin(req: ClaimAdminRequest): Promise { - return await this.request('POST', 'api/auth/claim-admin', req); - } - - async sshChallenge(): Promise { - return await this.request('POST', 'api/auth/ssh-challenge'); - } - - // SERVER - async getServerList(): Promise { - return await this.request('GET', 'api/server'); - } - - async createServer(req: CreateServerRequest): Promise { - return await this.request('POST', 'api/server', req); - } - - async getServerTree(serverId: string): Promise { - return await this.request('GET', `api/server/servers/${serverId}/tree/`); - } - - // CATEGORY - async getCategoryList(serverId?: string): Promise { - const path = serverId ? `api/category?server_id=${serverId}` : 'api/category'; - return await this.request('GET', path); - } - - async createCategory(req: CreateCategoryRequest): Promise { - return await this.request('POST', 'api/category', req); - } - - // CHANNEL - async getChannelList(): Promise { - return await this.request('GET', 'api/channel'); - } - - async createChannel(req: CreateChannelRequest): Promise { - return await this.request('POST', 'api/channel', req); - } - - // MESSAGE - async getMessageList(): Promise { - return await this.request('GET', 'api/message'); - } - - async createMessage(req: CreateMessageRequest): Promise { - return await this.request('POST', 'api/message', req); - } - - // USER - async getUserList(): Promise { - return await this.request('GET', 'api/user'); - } -} diff --git a/src/main.ts b/src/main.ts index 4145b57..b843b70 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,34 +1,78 @@ import './assets/main.css' import {createApp} from 'vue' -import {createRouter, createWebHistory} from 'vue-router' +import router from './router' import ui from '@nuxt/ui/vue-plugin' import App from './App.vue' import {createPinia} from "pinia"; +import {debug, error, info, trace, warn} from '@tauri-apps/plugin-log' +import {useGlobalStore} from "@/stores"; + +function getCallerLocation(): string { + const stack = new Error().stack ?? ''; + const lines = stack.split('\n'); + + for (const line of lines) { + if (line.includes('node_modules') || line.includes('main.ts')) { + continue; + } + + // Chromium/Firefox : " at fetchServers (http://localhost:1420/src/App.vue:39:18)" + // ou sans nom : " at http://localhost:1420/src/App.vue:39:18" + const chromiumMatch = line.match(/^\s+at\s+(?:\S+\s+)?\(?https?:\/\/[^/]+([^?]+?)(?:\?[^:]*)?:(\d+):\d+\)?$/); + if (chromiumMatch) { + return `${chromiumMatch[1]}:${chromiumMatch[2]}`; + } + + // WebKit : "fetchServers@http://localhost:1420/src/App.vue:39:18" + const webkitMatch = line.match(/@https?:\/\/[^/]+([^?]+?)(?:\?[^:]*)?:(\d+):\d+$/); + if (webkitMatch) { + return `${webkitMatch[1]}:${webkitMatch[2]}`; + } + } + + return 'unknown'; +} + +function forwardConsole( + fnName: 'log' | 'debug' | 'info' | 'warn' | 'error', + logger: (message: string) => Promise +) { + const original = console[fnName].bind(console); + console[fnName] = (...args: unknown[]) => { + original(...args); + + const location = getCallerLocation(); + const message = `[${location}] ` + args + .map(a => { + if (a instanceof Error) return `${a.message}`; + if (typeof a === 'object' && a !== null) return JSON.stringify(a); + return String(a); + }) + .join(' '); + + logger(message); + }; +} + +forwardConsole('log', trace); +forwardConsole('debug', debug); +forwardConsole('info', info); +forwardConsole('warn', warn); +forwardConsole('error', error); + +console.log('--- JS Logs Initialized (Forwarding enabled) ---'); +info('--- Direct Plugin Info: JS side ready ---'); const app = createApp(App) -const router = createRouter({ - routes: [ - {path: '/', component: () => import('./pages/index.vue')}, - {path: '/config', component: () => import('./pages/config.vue')}, - { - path: '/server/:server_id', - component: () => import('./pages/server_detail.vue'), - props: true, - children: [ - { - path: 'channel/:channel_id', - component: () => import('./pages/channel_detail.vue'), - props: true - }, - ] - } - ], - history: createWebHistory() -}) - app.use(router) app.use(ui) -app.use(createPinia()) + +const pinia = createPinia() +app.use(pinia) + +const globalStore = useGlobalStore() +await globalStore.init() + app.mount('#app') diff --git a/src/pages/config.vue b/src/pages/config.vue index 9f5c079..7749abb 100644 --- a/src/pages/config.vue +++ b/src/pages/config.vue @@ -1,97 +1,38 @@ @@ -102,139 +43,21 @@ onMounted(() => { Sauvegarder - - - - - + diff --git a/src/pages/index.vue b/src/pages/index.vue index 53c344f..a213da9 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -1,11 +1,43 @@ +
+
+

Ox-Speak

+

Votre plateforme de communication décentralisée.

+
- \ No newline at end of file + +
+ + + + \ No newline at end of file diff --git a/src/stores/config.ts b/src/stores/config.ts index 7e1134b..90b8c0c 100644 --- a/src/stores/config.ts +++ b/src/stores/config.ts @@ -1,11 +1,12 @@ import {defineStore} from 'pinia'; import {ref} from 'vue'; -import {type ConfigTree, createConfigClient} from '../config'; +import {type ConfigTree, createConfigClient} from '@/config'; export const useConfigStore = defineStore('config', () => { const client = createConfigClient(); const config = ref(null); const loading = ref(true); + const saving = ref(false); async function loadConfig() { loading.value = true; @@ -21,19 +22,63 @@ export const useConfigStore = defineStore('config', () => { config.value = newConfig; } + async function saveConfig() { + if (!config.value) return; + saving.value = true; + try { + await client.update(config.value); + // Re-fetch pour récupérer les clés auto-générées + await loadConfig(); + } finally { + saving.value = false; + } + } + + function addServer() { + if (!config.value) return; + config.value.servers.push({adresse: '', identity: ''} as any); + } + + function removeServer(index: number) { + if (!config.value) return; + config.value.servers.splice(index, 1); + } + + function addIdentity() { + if (!config.value) return; + config.value.identities.push({ + id: crypto.randomUUID(), + username: '', + private_key: '', + mode: 'private_key_path' + } as any); + } + + function removeIdentity(index: number) { + if (!config.value) return; + config.value.identities.splice(index, 1); + } + // Initialiser la configuration et écouter les changements async function init() { await loadConfig(); await client.onChanged((updatedConfig) => { config.value = updatedConfig; }); + console.debug('Config store initialized'); } return { config, loading, + saving, loadConfig, updateConfig, + saveConfig, + addServer, + removeServer, + addIdentity, + removeIdentity, init, }; }); diff --git a/src/stores/global.ts b/src/stores/global.ts index eeccc75..0e3ab0b 100644 --- a/src/stores/global.ts +++ b/src/stores/global.ts @@ -1,9 +1,25 @@ import {defineStore} from 'pinia'; import {ref} from 'vue'; +import {useConfigStore} from './config'; export const useGlobalStore = defineStore('global', () => { const loading = ref(false); const theme = ref('dark'); + const isInitialized = ref(false); + + async function init() { + console.debug('Initializing global store...'); + if (isInitialized.value) return; + + // Initialisation des stores requis au boot + const configStore = useConfigStore(); + await configStore.init(); + + // Autres initialisations peuvent être ajoutées ici plus tard + + isInitialized.value = true; + console.debug('Global store initialized'); + } function setLoading(value: boolean) { loading.value = value; @@ -16,7 +32,9 @@ export const useGlobalStore = defineStore('global', () => { return { loading, theme, + isInitialized, setLoading, toggleTheme, + init, }; }); diff --git a/src/ws/client.ts b/src/ws/client.ts index 2e32577..38169ab 100644 --- a/src/ws/client.ts +++ b/src/ws/client.ts @@ -1,54 +1,10 @@ -import {invoke} from '@tauri-apps/api/core'; -import {listen} from '@tauri-apps/api/event'; -import type {IWsClient, WsEvent} from './types'; +import type {IWsClient} from './types'; /** - * Implémentation Tauri du client WebSocket. - * Utilise le backend Rust pour maintenir la connexion. + * Unified WebSocket Client for OxSpeak. + * Implemented ONLY in TypeScript. */ -export class TauriWsClient implements IWsClient { - private serverUrl: string | null = null; - - async connect(serverUrl: string, token?: string): Promise { - this.serverUrl = serverUrl; - await invoke('ws_connect', {serverUrl, token}); - } - - async send(message: any): Promise { - if (!this.serverUrl) throw new Error('Non connecté'); - const msgString = typeof message === 'string' ? message : JSON.stringify(message); - await invoke('ws_send', {serverUrl: this.serverUrl, message: msgString}); - } - - async disconnect(): Promise { - if (this.serverUrl) { - await invoke('ws_disconnect', {serverUrl: this.serverUrl}); - this.serverUrl = null; - } - } - - onMessage(callback: (payload: any) => void): () => void { - let unlisten: (() => void) | null = null; - - listen('ws-message', (event) => { - // Filtrer par serverUrl si nécessaire - if (this.serverUrl && event.payload.server_url === this.serverUrl) { - callback(event.payload.payload); - } - }).then((fn) => { - unlisten = fn; - }); - - return () => { - if (unlisten) unlisten(); - }; - } -} - -/** - * Implémentation Web (standard) du client WebSocket. - */ -export class WebWsClient implements IWsClient { +export class WsClient implements IWsClient { private ws: WebSocket | null = null; private messageCallbacks: Set<(payload: any) => void> = new Set(); private serverUrl: string | null = null; @@ -65,19 +21,36 @@ export class WebWsClient implements IWsClient { } return new Promise((resolve, reject) => { - this.ws = new WebSocket(wsUrl); + try { + this.ws = new WebSocket(wsUrl); - this.ws.onopen = () => resolve(); - this.ws.onerror = (err) => reject(err); - this.ws.onmessage = (event) => { - try { - const payload = JSON.parse(event.data); - this.messageCallbacks.forEach((cb) => cb(payload)); - } catch (e) { - // Si ce n'est pas du JSON, on renvoie la data brute - this.messageCallbacks.forEach((cb) => cb(event.data)); - } - }; + this.ws.onopen = () => { + console.log('WebSocket connected to', wsUrl); + resolve(); + }; + + this.ws.onerror = (err) => { + console.error('WebSocket error:', err); + reject(err); + }; + + this.ws.onmessage = (event) => { + try { + const payload = JSON.parse(event.data); + this.messageCallbacks.forEach((cb) => cb(payload)); + } catch (e) { + // Si ce n'est pas du JSON, on renvoie la data brute + this.messageCallbacks.forEach((cb) => cb(event.data)); + } + }; + + this.ws.onclose = (event) => { + console.log('WebSocket closed:', event.code, event.reason); + this.ws = null; + }; + } catch (e) { + reject(e); + } }); } @@ -103,11 +76,9 @@ export class WebWsClient implements IWsClient { } /** - * Factory pour créer le client approprié. + * Factory for creating the WebSocket client. + * Always returns the unified TS implementation. */ export function createWsClient(): IWsClient { - if ((window as any).__TAURI_INTERNALS__) { - return new TauriWsClient(); - } - return new WebWsClient(); + return new WsClient(); } diff --git a/tsconfig.app.json b/tsconfig.app.json index a8b0bbc..eb20cc5 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -3,9 +3,12 @@ "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, @@ -13,17 +16,26 @@ "moduleDetection": "force", "noEmit": true, "jsx": "preserve", - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - /* Paths */ "paths": { - "#build/ui/*": ["./node_modules/.nuxt-ui/ui/*"] + "#build/ui/*": [ + "./node_modules/.nuxt-ui/ui/*" + ], + "@/*": [ + "src/*" + ] } }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"] + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "auto-imports.d.ts", + "components.d.ts" + ] } \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json index 4b75618..e3bbcea 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,27 +1,33 @@ { "compilerOptions": { "target": "ES2022", - "lib": ["ES2023"], + "lib": [ + "ES2023" + ], "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - /* Paths */ "paths": { - "#build/ui": ["./node_modules/.nuxt-ui/ui"] + "#build/ui": [ + "./node_modules/.nuxt-ui/ui" + ], + "@/*": [ + "src/*" + ] } }, - "include": ["vite.config.ts"] + "include": [ + "vite.config.ts" + ] } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 03778ba..2e68ff1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from "vite"; +import {defineConfig} from "vite"; import vue from "@vitejs/plugin-vue"; import ui from '@nuxt/ui/vite' @@ -11,7 +11,11 @@ export default defineConfig(async () => ({ vue(), ui() ], - + resolve: { + alias: { + '@': '/src' + } + }, // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // // 1. prevent vite from obscuring rust errors @@ -24,10 +28,10 @@ export default defineConfig(async () => ({ cors: true, hmr: host ? { - protocol: "ws", - host, - port: 1421, - } + protocol: "ws", + host, + port: 1421, + } : undefined, watch: { // 3. tell vite to ignore watching `src-tauri` diff --git a/yarn.lock b/yarn.lock index 683299e..78561b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1354,6 +1354,15 @@ __metadata: languageName: node linkType: hard +"@tauri-apps/plugin-log@npm:~2": + version: 2.8.0 + resolution: "@tauri-apps/plugin-log@npm:2.8.0" + dependencies: + "@tauri-apps/api": "npm:^2.8.0" + checksum: 10c0/04c21bae0174de8a394760e40986ae5f28e07830b9e7388dd9ddd4796c4b4eac7f80807a90498d5896a6113134efb1ddab1038be9c6ba2095c31b75f1b85dc7f + languageName: node + linkType: hard + "@tauri-apps/plugin-opener@npm:^2": version: 2.5.3 resolution: "@tauri-apps/plugin-opener@npm:2.5.3" @@ -3869,6 +3878,7 @@ __metadata: "@nuxt/ui": "npm:^4.5.1" "@tauri-apps/api": "npm:^2" "@tauri-apps/cli": "npm:^2" + "@tauri-apps/plugin-log": "npm:~2" "@tauri-apps/plugin-opener": "npm:^2" "@vitejs/plugin-vue": "npm:^6.0.5" pinia: "npm:^3.0.4"