pré-rework
This commit is contained in:
17
package.json
17
package.json
@@ -10,18 +10,19 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/ui": "^4.3.0",
|
"@nuxt/ui": "^4.5.1",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"tailwindcss": "^4.1.18",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.13",
|
"tailwindcss": "^4.2.2",
|
||||||
"vue-router": "^4.6.4"
|
"vue": "^3.5.30",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^6.0.5",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^6.0.3",
|
"vite": "^8.0.1",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^3.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1091
src-tauri/Cargo.lock
generated
1091
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,13 @@ opusic-sys = "0.6"
|
|||||||
tokio = { version = "1.50", features = ["full"] }
|
tokio = { version = "1.50", features = ["full"] }
|
||||||
reqwest = { version = "0.13", features = ["json", "rustls"] }
|
reqwest = { version = "0.13", features = ["json", "rustls"] }
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
|
tokio-tungstenite = { version = "0.29", features = ["rustls-tls-native-roots"] }
|
||||||
|
futures-util = "0.3"
|
||||||
|
url = "2.5"
|
||||||
|
|
||||||
figment = "0.10.19"
|
figment = "0.10.19"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
toml = "1.0.7+spec-1.1.0"
|
toml = "1.0.7+spec-1.1.0"
|
||||||
ssh-key = { version = "0.7.0-rc.9", features = ["default", "crypto"] }
|
ssh-key = { version = "0.7.0-rc.9", features = ["default", "crypto"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
rand = "0.10"
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
pub mod client;
|
|
||||||
pub mod commands;
|
|
||||||
pub mod models;
|
|
||||||
pub mod state;
|
|
||||||
|
|
||||||
pub use client::{ApiClient, ApiError, ApiResult};
|
|
||||||
pub use state::ApiManager;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
use crate::api::client::ApiClient;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Gère plusieurs instances de ApiClient (une par serveur).
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct ApiManager {
|
|
||||||
clients: RwLock<HashMap<String, ApiClient>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiManager {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
clients: RwLock::new(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Récupère ou crée un client pour une base_url donnée.
|
|
||||||
pub fn get_or_create_client(&self, base_url: String) -> ApiClient {
|
|
||||||
let mut clients = self.clients.write();
|
|
||||||
if let Some(client) = clients.get(&base_url) {
|
|
||||||
client.clone()
|
|
||||||
} else {
|
|
||||||
let client = ApiClient::new(base_url.clone());
|
|
||||||
clients.insert(base_url, client.clone());
|
|
||||||
client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supprime un client.
|
|
||||||
pub fn remove_client(&self, base_url: &str) {
|
|
||||||
let mut clients = self.clients.write();
|
|
||||||
clients.remove(base_url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
135
src-tauri/src/api/web-client.ts
Normal file
135
src-tauri/src/api/web-client.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import type {IApiClient} from './client';
|
||||||
|
import type {
|
||||||
|
CategoryResponse,
|
||||||
|
ChannelResponse,
|
||||||
|
ClaimAdminRequest,
|
||||||
|
CreateCategoryRequest,
|
||||||
|
CreateChannelRequest,
|
||||||
|
CreateMessageRequest,
|
||||||
|
CreateServerRequest,
|
||||||
|
LoginRequest,
|
||||||
|
LoginResponse,
|
||||||
|
MessageResponse,
|
||||||
|
ServerResponse,
|
||||||
|
SshChallengeResponse,
|
||||||
|
UserResponse,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation Web (standard) de l'interface API.
|
||||||
|
* Elle utilise fetch() pour communiquer directement avec le serveur.
|
||||||
|
*/
|
||||||
|
export class WebApiClient implements IApiClient {
|
||||||
|
private token: string | null = null;
|
||||||
|
|
||||||
|
constructor(public readonly baseUrl: string) {
|
||||||
|
this.token = localStorage.getItem(`token_${baseUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(method: string, path: string, body?: any): Promise<T> {
|
||||||
|
// Nettoyer l'URL
|
||||||
|
const cleanBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||||
|
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||||
|
const url = `${cleanBaseUrl}/${cleanPath}`;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.token) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorData;
|
||||||
|
try {
|
||||||
|
errorData = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
errorData = {message: await response.text()};
|
||||||
|
}
|
||||||
|
throw new Error(errorData.message || `Erreur HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return {} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setToken(token: string) {
|
||||||
|
this.token = token;
|
||||||
|
localStorage.setItem(`token_${this.baseUrl}`, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUTH
|
||||||
|
async login(req: LoginRequest): Promise<LoginResponse> {
|
||||||
|
const res = await this.request<LoginResponse>('POST', 'api/auth/login', req);
|
||||||
|
this.setToken(res.token);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyToken(): Promise<UserResponse> {
|
||||||
|
return await this.request<UserResponse>('GET', 'api/auth/me');
|
||||||
|
}
|
||||||
|
|
||||||
|
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
||||||
|
return await this.request<void>('POST', 'api/auth/claim-admin', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sshChallenge(): Promise<SshChallengeResponse> {
|
||||||
|
return await this.request<SshChallengeResponse>('POST', 'api/auth/ssh-challenge');
|
||||||
|
}
|
||||||
|
|
||||||
|
// SERVER
|
||||||
|
async getServerList(): Promise<ServerResponse[]> {
|
||||||
|
return await this.request<ServerResponse[]>('GET', 'api/server');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
||||||
|
return await this.request<ServerResponse>('POST', 'api/server', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getServerTree(serverId: string): Promise<any> {
|
||||||
|
return await this.request<any>('GET', `api/server/servers/${serverId}/tree/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CATEGORY
|
||||||
|
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
||||||
|
const path = serverId ? `api/category?server_id=${serverId}` : 'api/category';
|
||||||
|
return await this.request<CategoryResponse[]>('GET', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
||||||
|
return await this.request<CategoryResponse>('POST', 'api/category', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHANNEL
|
||||||
|
async getChannelList(): Promise<ChannelResponse[]> {
|
||||||
|
return await this.request<ChannelResponse[]>('GET', 'api/channel');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
||||||
|
return await this.request<ChannelResponse>('POST', 'api/channel', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MESSAGE
|
||||||
|
async getMessageList(): Promise<MessageResponse[]> {
|
||||||
|
return await this.request<MessageResponse[]>('GET', 'api/message');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
||||||
|
return await this.request<MessageResponse>('POST', 'api/message', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// USER
|
||||||
|
async getUserList(): Promise<UserResponse[]> {
|
||||||
|
return await this.request<UserResponse[]>('GET', 'api/user');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
use crate::app::state::AppState;
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
use tauri::AppHandle;
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
pub struct OxSpeakApp {
|
pub struct OxSpeakApp {
|
||||||
tauri_handle: AppHandle,
|
tauri_handle: AppHandle,
|
||||||
@@ -16,7 +17,33 @@ impl OxSpeakApp {
|
|||||||
|
|
||||||
pub async fn run(&self) {
|
pub async fn run(&self) {
|
||||||
println!("Starting OxSpeak backend...");
|
println!("Starting OxSpeak backend...");
|
||||||
// infinite loop
|
|
||||||
println!("End of OxSpeak backend.");
|
let state = self.tauri_handle.state::<AppState>();
|
||||||
|
let config = self.config.get_config();
|
||||||
|
|
||||||
|
// Les connexions seront initialisé dans la partie JS, pour garder la cross-compatibilité entre Tauri et Web
|
||||||
|
// for server in config.servers {
|
||||||
|
// println!("Initialisation du client pour {}", server.adresse);
|
||||||
|
// let client = state
|
||||||
|
// .client_manager
|
||||||
|
// .add_client(server.adresse.clone(), server.token.clone());
|
||||||
|
//
|
||||||
|
// // Tentative de connexion WebSocket si un token est présent
|
||||||
|
// if server.token.is_some() {
|
||||||
|
// println!("Tentative de connexion WebSocket pour {}", server.adresse);
|
||||||
|
// let handle_clone = self.tauri_handle.clone();
|
||||||
|
// let server_addr = server.adresse.clone();
|
||||||
|
// tokio::spawn(async move {
|
||||||
|
// if let Err(e) = client.connect_ws(handle_clone).await {
|
||||||
|
// eprintln!("Echec de connexion automatique pour {}: {}", server_addr, e);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Boucle infinie pour garder le thread en vie
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
use crate::api::state::ApiManager;
|
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
|
use crate::network::ClientManager;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
/// État global de l'application.
|
/// État global de l'application.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub api: ApiManager,
|
|
||||||
pub config: OnceLock<ConfigManager>,
|
pub config: OnceLock<ConfigManager>,
|
||||||
|
pub client_manager: ClientManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
api: ApiManager::new(),
|
|
||||||
config: OnceLock::new(),
|
config: OnceLock::new(),
|
||||||
|
client_manager: ClientManager::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,14 @@ pub async fn config_update(
|
|||||||
new_config: ConfigTree,
|
new_config: ConfigTree,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let config = state.config.get().ok_or("Config non initialisée")?;
|
let config = state.config.get().ok_or("Config non initialisée")?;
|
||||||
|
println!("{:?}", new_config);
|
||||||
config
|
config
|
||||||
.update_config(new_config)
|
.update_config(new_config)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn generate_ssh_key() -> Result<String, String> {
|
||||||
|
crate::config::config::generate_ssh_key_base64().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ssh_key::PrivateKey;
|
use ssh_key::{Algorithm, PrivateKey};
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -25,6 +25,8 @@ pub struct ConfigTree {
|
|||||||
pub struct ConfigServer {
|
pub struct ConfigServer {
|
||||||
pub adresse: String,
|
pub adresse: String,
|
||||||
pub identity: String,
|
pub identity: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
@@ -32,12 +34,14 @@ pub struct ConfigServer {
|
|||||||
pub enum IdentityMode {
|
pub enum IdentityMode {
|
||||||
PrivateKeyPath,
|
PrivateKeyPath,
|
||||||
PrivateKeyBase64,
|
PrivateKeyBase64,
|
||||||
|
Login,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
pub struct IdentityConfig {
|
pub struct IdentityConfig {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
#[serde(default)]
|
||||||
pub private_key: String,
|
pub private_key: String,
|
||||||
pub mode: IdentityMode,
|
pub mode: IdentityMode,
|
||||||
}
|
}
|
||||||
@@ -47,7 +51,18 @@ impl ConfigManager {
|
|||||||
self.tree_config.lock().clone()
|
self.tree_config.lock().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_config(&self, new_config: ConfigTree) -> Result<(), std::io::Error> {
|
pub async fn update_config(&self, mut new_config: ConfigTree) -> Result<(), std::io::Error> {
|
||||||
|
// Auto-génération des clés si mode Base64 et champ vide
|
||||||
|
for identity in &mut new_config.identities {
|
||||||
|
if matches!(identity.mode, IdentityMode::PrivateKeyBase64)
|
||||||
|
&& identity.private_key.trim().is_empty()
|
||||||
|
{
|
||||||
|
if let Ok(key) = generate_ssh_key_base64() {
|
||||||
|
identity.private_key = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut lock = self.tree_config.lock();
|
let mut lock = self.tree_config.lock();
|
||||||
*lock = new_config.clone();
|
*lock = new_config.clone();
|
||||||
@@ -67,7 +82,21 @@ impl ConfigManager {
|
|||||||
|
|
||||||
if config_path.exists() && config_path.is_file() {
|
if config_path.exists() && config_path.is_file() {
|
||||||
let config_content = fs::read_to_string(config_path.clone()).await?;
|
let config_content = fs::read_to_string(config_path.clone()).await?;
|
||||||
if let Ok(tree_config) = toml::from_str::<ConfigTree>(&config_content) {
|
if let Ok(mut tree_config) = toml::from_str::<ConfigTree>(&config_content) {
|
||||||
|
// Si aucune identité n'existe, on en crée une par défaut
|
||||||
|
if tree_config.identities.is_empty() {
|
||||||
|
let username = std::env::var("USER")
|
||||||
|
.or_else(|_| std::env::var("USERNAME"))
|
||||||
|
.unwrap_or_else(|_| "default".to_string());
|
||||||
|
|
||||||
|
tree_config.identities.push(IdentityConfig {
|
||||||
|
id: "default".to_string(),
|
||||||
|
username,
|
||||||
|
private_key: String::new(),
|
||||||
|
mode: IdentityMode::Login,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
tree_config: Arc::new(Mutex::new(tree_config)),
|
tree_config: Arc::new(Mutex::new(tree_config)),
|
||||||
path: config_path,
|
path: config_path,
|
||||||
@@ -82,9 +111,21 @@ impl ConfigManager {
|
|||||||
async fn create(config_dir: PathBuf, app_handle: AppHandle) -> Result<Self, std::io::Error> {
|
async fn create(config_dir: PathBuf, app_handle: AppHandle) -> Result<Self, std::io::Error> {
|
||||||
let config_path = config_dir.join("config.toml");
|
let config_path = config_dir.join("config.toml");
|
||||||
|
|
||||||
|
// Obtenir le nom de l'utilisateur de session (cross-platform)
|
||||||
|
let username = std::env::var("USER")
|
||||||
|
.or_else(|_| std::env::var("USERNAME"))
|
||||||
|
.unwrap_or_else(|_| "default".to_string());
|
||||||
|
|
||||||
|
let default_identity = IdentityConfig {
|
||||||
|
id: "default".to_string(),
|
||||||
|
username,
|
||||||
|
private_key: String::new(),
|
||||||
|
mode: IdentityMode::Login,
|
||||||
|
};
|
||||||
|
|
||||||
let tree_config = ConfigTree {
|
let tree_config = ConfigTree {
|
||||||
servers: Vec::new(),
|
servers: Vec::new(),
|
||||||
identities: Vec::new(),
|
identities: vec![default_identity],
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = ConfigManager {
|
let config = ConfigManager {
|
||||||
@@ -137,6 +178,16 @@ impl IdentityConfig {
|
|||||||
PrivateKey::from_openssh(&key_content)
|
PrivateKey::from_openssh(&key_content)
|
||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
||||||
}
|
}
|
||||||
|
IdentityMode::Login => {
|
||||||
|
// Pas de clé privée pour ce mode
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Unsupported,
|
||||||
|
format!(
|
||||||
|
"Le mode d'authentification {:?} n'utilise pas de clé privée.",
|
||||||
|
self.mode
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +204,16 @@ impl ConfigServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_ssh_key_base64() -> Result<String, std::io::Error> {
|
||||||
|
let key = PrivateKey::random(&mut rand::rng(), Algorithm::Ed25519)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||||
|
let openssh_content = key
|
||||||
|
.to_openssh(ssh_key::LineEnding::LF)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||||
|
// On encode en base64 car c'est ce qu'attend le mode PrivateKeyBase64 actuel
|
||||||
|
Ok(general_purpose::STANDARD.encode(openssh_content.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
fn expand_path(path: &str) -> PathBuf {
|
fn expand_path(path: &str) -> PathBuf {
|
||||||
if path.starts_with("~/") {
|
if path.starts_with("~/") {
|
||||||
if let Some(home) = std::env::var_os("HOME") {
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ mod app;
|
|||||||
pub mod tauri_app;
|
pub mod tauri_app;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub mod api;
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod network;
|
||||||
|
|||||||
131
src-tauri/src/network/client.rs
Normal file
131
src-tauri/src/network/client.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use crate::network::http::api::ApiClient;
|
||||||
|
use crate::network::http::ws::client::WsClient;
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Client {
|
||||||
|
pub base_url: String,
|
||||||
|
pub api: ApiClient,
|
||||||
|
pub ws_sender: Arc<RwLock<Option<mpsc::UnboundedSender<String>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(base_url: String, token: Option<String>) -> Self {
|
||||||
|
let api = ApiClient::new(base_url.clone());
|
||||||
|
if let Some(t) = token {
|
||||||
|
api.set_token(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
base_url,
|
||||||
|
api,
|
||||||
|
ws_sender: Arc::new(RwLock::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_ws(&self, app_handle: AppHandle) -> Result<(), String> {
|
||||||
|
// 1. Vérifier le token via HTTP avant
|
||||||
|
if let Some(_token) = self.api.get_token() {
|
||||||
|
println!("Vérification du token pour {}", self.base_url);
|
||||||
|
match self.api.verify_token().await {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("Token valide pour {}", self.base_url);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Token invalide pour {}: {}", self.base_url, e);
|
||||||
|
// Ici on pourrait déclencher un événement pour demander une ré-authentification
|
||||||
|
// Pour le moment on s'arrête là ou on tente quand même la connexion WS
|
||||||
|
// selon le besoin. Mais l'utilisateur dit "relance une authentification http"
|
||||||
|
return Err("Token invalide, veuillez vous reconnecter".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err("Aucun token disponible".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ws_sender_lock = self.ws_sender.write();
|
||||||
|
|
||||||
|
// Fermer l'ancienne connexion si elle existe
|
||||||
|
if ws_sender_lock.is_some() {
|
||||||
|
println!("Déconnexion de l'ancien WebSocket pour {}", self.base_url);
|
||||||
|
*ws_sender_lock = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
let token = self.api.get_token();
|
||||||
|
let client = WsClient::new(self.base_url.clone(), token, app_handle);
|
||||||
|
|
||||||
|
*ws_sender_lock = Some(tx);
|
||||||
|
|
||||||
|
// Lancer la tâche de connexion en arrière-plan
|
||||||
|
tokio::spawn(async move {
|
||||||
|
client.run(rx).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_ws(&self, message: String) -> Result<(), String> {
|
||||||
|
let ws_sender_lock = self.ws_sender.read();
|
||||||
|
if let Some(tx) = ws_sender_lock.as_ref() {
|
||||||
|
tx.send(message)
|
||||||
|
.map_err(|e| format!("Erreur d'envoi WebSocket: {}", e))
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"Pas de connexion WebSocket active pour {}",
|
||||||
|
self.base_url
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect_ws(&self) {
|
||||||
|
let mut ws_sender_lock = self.ws_sender.write();
|
||||||
|
*ws_sender_lock = None;
|
||||||
|
println!("WebSocket déconnecté pour {}", self.base_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ClientManager {
|
||||||
|
clients: RwLock<HashMap<String, Arc<Client>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
clients: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_or_create_client(&self, base_url: String) -> Arc<Client> {
|
||||||
|
let mut clients = self.clients.write();
|
||||||
|
if let Some(client) = clients.get(&base_url) {
|
||||||
|
client.clone()
|
||||||
|
} else {
|
||||||
|
let client = Arc::new(Client::new(base_url.clone(), None));
|
||||||
|
clients.insert(base_url, client.clone());
|
||||||
|
client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_client(&self, base_url: String, token: Option<String>) -> Arc<Client> {
|
||||||
|
let mut clients = self.clients.write();
|
||||||
|
let client = Arc::new(Client::new(base_url.clone(), token));
|
||||||
|
clients.insert(base_url, client.clone());
|
||||||
|
client
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_client(&self, base_url: &str) {
|
||||||
|
let mut clients = self.clients.write();
|
||||||
|
clients.remove(base_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_client(&self, base_url: &str) -> Option<Arc<Client>> {
|
||||||
|
let clients = self.clients.read();
|
||||||
|
clients.get(base_url).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::api::models::*;
|
use crate::network::http::api::models::*;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use reqwest::{header, Client, Method, RequestBuilder, Response};
|
use reqwest::{header, Client, Method, RequestBuilder, Response};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
@@ -152,6 +152,10 @@ impl ApiClient {
|
|||||||
// --- ECOSYSTEMS ---
|
// --- ECOSYSTEMS ---
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
|
pub async fn verify_token(&self) -> ApiResult<UserResponse> {
|
||||||
|
self.get("api/auth/me").await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn login(&self, req: &LoginRequest) -> ApiResult<LoginResponse> {
|
pub async fn login(&self, req: &LoginRequest) -> ApiResult<LoginResponse> {
|
||||||
self.post("api/auth/login", req).await
|
self.post("api/auth/login", req).await
|
||||||
}
|
}
|
||||||
@@ -173,6 +177,11 @@ impl ApiClient {
|
|||||||
self.post("api/server", req).await
|
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
|
// Category
|
||||||
pub async fn category_list(&self, server_id: Option<&str>) -> ApiResult<Vec<CategoryResponse>> {
|
pub async fn category_list(&self, server_id: Option<&str>) -> ApiResult<Vec<CategoryResponse>> {
|
||||||
let path = if let Some(id) = server_id {
|
let path = if let Some(id) = server_id {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::api::models::*;
|
|
||||||
use crate::api::ApiResult;
|
|
||||||
use crate::app::state::AppState;
|
use crate::app::state::AppState;
|
||||||
|
use crate::network::http::api::client::ApiResult;
|
||||||
|
use crate::network::http::api::models::*;
|
||||||
use tauri::{command, State};
|
use tauri::{command, State};
|
||||||
|
|
||||||
// --- AUTH ---
|
// --- AUTH ---
|
||||||
@@ -11,20 +11,29 @@ pub async fn api_login(
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
req: LoginRequest,
|
req: LoginRequest,
|
||||||
) -> ApiResult<LoginResponse> {
|
) -> ApiResult<LoginResponse> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
let res = client.login(&req).await?;
|
let res = client.api.login(&req).await?;
|
||||||
client.set_token(res.token.clone());
|
client.api.set_token(res.token.clone());
|
||||||
Ok(res)
|
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]
|
#[command]
|
||||||
pub async fn api_claim_admin(
|
pub async fn api_claim_admin(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
req: ClaimAdminRequest,
|
req: ClaimAdminRequest,
|
||||||
) -> ApiResult<()> {
|
) -> ApiResult<()> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.claim_admin(&req).await
|
client.api.claim_admin(&req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -32,8 +41,8 @@ pub async fn api_ssh_challenge(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) -> ApiResult<SshChallengeResponse> {
|
) -> ApiResult<SshChallengeResponse> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.ssh_challenge().await
|
client.api.ssh_challenge().await
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SERVER ---
|
// --- SERVER ---
|
||||||
@@ -43,8 +52,8 @@ pub async fn api_server_list(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) -> ApiResult<Vec<ServerResponse>> {
|
) -> ApiResult<Vec<ServerResponse>> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.server_list().await
|
client.api.server_list().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -53,8 +62,18 @@ pub async fn api_server_create(
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
req: CreateServerRequest,
|
req: CreateServerRequest,
|
||||||
) -> ApiResult<ServerResponse> {
|
) -> ApiResult<ServerResponse> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.server_create(&req).await
|
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 ---
|
// --- CATEGORY ---
|
||||||
@@ -65,8 +84,8 @@ pub async fn api_category_list(
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
server_id: Option<String>,
|
server_id: Option<String>,
|
||||||
) -> ApiResult<Vec<CategoryResponse>> {
|
) -> ApiResult<Vec<CategoryResponse>> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.category_list(server_id.as_deref()).await
|
client.api.category_list(server_id.as_deref()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -75,8 +94,8 @@ pub async fn api_category_create(
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
req: CreateCategoryRequest,
|
req: CreateCategoryRequest,
|
||||||
) -> ApiResult<CategoryResponse> {
|
) -> ApiResult<CategoryResponse> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.category_create(&req).await
|
client.api.category_create(&req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CHANNEL ---
|
// --- CHANNEL ---
|
||||||
@@ -86,8 +105,8 @@ pub async fn api_channel_list(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) -> ApiResult<Vec<ChannelResponse>> {
|
) -> ApiResult<Vec<ChannelResponse>> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.channel_list().await
|
client.api.channel_list().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -96,8 +115,8 @@ pub async fn api_channel_create(
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
req: CreateChannelRequest,
|
req: CreateChannelRequest,
|
||||||
) -> ApiResult<ChannelResponse> {
|
) -> ApiResult<ChannelResponse> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.channel_create(&req).await
|
client.api.channel_create(&req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MESSAGE ---
|
// --- MESSAGE ---
|
||||||
@@ -107,8 +126,8 @@ pub async fn api_message_list(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) -> ApiResult<Vec<MessageResponse>> {
|
) -> ApiResult<Vec<MessageResponse>> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.message_list().await
|
client.api.message_list().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -117,8 +136,8 @@ pub async fn api_message_create(
|
|||||||
base_url: String,
|
base_url: String,
|
||||||
req: CreateMessageRequest,
|
req: CreateMessageRequest,
|
||||||
) -> ApiResult<MessageResponse> {
|
) -> ApiResult<MessageResponse> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.message_create(&req).await
|
client.api.message_create(&req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- USER ---
|
// --- USER ---
|
||||||
@@ -128,6 +147,6 @@ pub async fn api_user_list(
|
|||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) -> ApiResult<Vec<UserResponse>> {
|
) -> ApiResult<Vec<UserResponse>> {
|
||||||
let client = state.api.get_or_create_client(base_url);
|
let client = state.client_manager.get_or_create_client(base_url);
|
||||||
client.user_list().await
|
client.api.user_list().await
|
||||||
}
|
}
|
||||||
5
src-tauri/src/network/http/api/mod.rs
Normal file
5
src-tauri/src/network/http/api/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod commands;
|
||||||
|
pub mod models;
|
||||||
|
|
||||||
|
pub use client::ApiClient;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod ws;
|
||||||
|
|||||||
106
src-tauri/src/network/http/ws/client.rs
Normal file
106
src-tauri/src/network/http/ws/client.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WsEvent {
|
||||||
|
pub server_url: String,
|
||||||
|
pub payload: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WsClient {
|
||||||
|
server_url: String,
|
||||||
|
token: Option<String>,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsClient {
|
||||||
|
pub fn new(server_url: String, token: Option<String>, app_handle: AppHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
server_url,
|
||||||
|
token,
|
||||||
|
app_handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(self, mut rx: mpsc::UnboundedReceiver<String>) {
|
||||||
|
let ws_url = self.prepare_url();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
println!("Tentative de connexion WebSocket à {}", ws_url);
|
||||||
|
|
||||||
|
match connect_async(&ws_url).await {
|
||||||
|
Ok((ws_stream, _)) => {
|
||||||
|
println!("Connecté au WebSocket: {}", ws_url);
|
||||||
|
let (mut write, mut read) = ws_stream.split();
|
||||||
|
|
||||||
|
// Envoyer le token si présent pour l'auth initiale si nécessaire par le protocole
|
||||||
|
// Ici on assume que le token est passé en query param ou via un message initial
|
||||||
|
// Pour le moment, on se contente de la connexion.
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
msg = read.next() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(Message::Text(text))) => {
|
||||||
|
let payload = serde_json::from_str::<serde_json::Value>(&text)
|
||||||
|
.unwrap_or_else(|_| serde_json::Value::String(text.to_string()));
|
||||||
|
|
||||||
|
let _ = self.app_handle.emit("ws-message", WsEvent {
|
||||||
|
server_url: self.server_url.clone(),
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(Ok(Message::Close(_))) | None => {
|
||||||
|
println!("WebSocket fermé par le serveur");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
eprintln!("Erreur WebSocket: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(to_send) = rx.recv() => {
|
||||||
|
if let Err(e) = write.send(Message::Text(to_send.into())).await {
|
||||||
|
eprintln!("Erreur lors de l'envoi WebSocket: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Erreur de connexion WebSocket: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconnexion après un délai
|
||||||
|
sleep(Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_url(&self) -> String {
|
||||||
|
let mut url = self
|
||||||
|
.server_url
|
||||||
|
.replace("http://", "ws://")
|
||||||
|
.replace("https://", "wss://");
|
||||||
|
if !url.ends_with("/ws") {
|
||||||
|
url = format!("{}/ws", url.trim_end_matches('/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = &self.token {
|
||||||
|
if url.contains('?') {
|
||||||
|
url = format!("{}&token={}", url, token);
|
||||||
|
} else {
|
||||||
|
url = format!("{}?token={}", url, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src-tauri/src/network/http/ws/commands.rs
Normal file
33
src-tauri/src/network/http/ws/commands.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use crate::app::state::AppState;
|
||||||
|
use tauri::{command, AppHandle, State};
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn ws_connect(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
server_url: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let client = state.client_manager.get_or_create_client(server_url);
|
||||||
|
client.connect_ws(app_handle).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn ws_send(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
server_url: String,
|
||||||
|
message: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(client) = state.client_manager.get_client(&server_url) {
|
||||||
|
client.send_ws(message)
|
||||||
|
} else {
|
||||||
|
Err(format!("Client non trouvé pour {}", server_url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn ws_disconnect(state: State<'_, AppState>, server_url: String) -> Result<(), String> {
|
||||||
|
if let Some(client) = state.client_manager.get_client(&server_url) {
|
||||||
|
client.disconnect_ws();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
2
src-tauri/src/network/http/ws/mod.rs
Normal file
2
src-tauri/src/network/http/ws/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod commands;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod http;
|
||||||
|
pub mod udp;
|
||||||
|
|
||||||
|
pub use client::ClientManager;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::api::commands::*;
|
|
||||||
use crate::app::ox_speak_app::OxSpeakApp;
|
use crate::app::ox_speak_app::OxSpeakApp;
|
||||||
use crate::app::state::AppState;
|
use crate::app::state::AppState;
|
||||||
use crate::config::commands::*;
|
use crate::config::commands::*;
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
|
use crate::network::http::api::commands::*;
|
||||||
|
use crate::network::http::ws::commands::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{Manager, WindowEvent};
|
use tauri::{Manager, WindowEvent};
|
||||||
|
|
||||||
@@ -23,10 +24,12 @@ pub async fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
api_login,
|
api_login,
|
||||||
|
api_verify_token,
|
||||||
api_claim_admin,
|
api_claim_admin,
|
||||||
api_ssh_challenge,
|
api_ssh_challenge,
|
||||||
api_server_list,
|
api_server_list,
|
||||||
api_server_create,
|
api_server_create,
|
||||||
|
api_server_tree,
|
||||||
api_category_list,
|
api_category_list,
|
||||||
api_category_create,
|
api_category_create,
|
||||||
api_channel_list,
|
api_channel_list,
|
||||||
@@ -36,6 +39,10 @@ pub async fn run() {
|
|||||||
api_user_list,
|
api_user_list,
|
||||||
config_get,
|
config_get,
|
||||||
config_update,
|
config_update,
|
||||||
|
generate_ssh_key,
|
||||||
|
ws_connect,
|
||||||
|
ws_send,
|
||||||
|
ws_disconnect,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|||||||
28
src/App.vue
28
src/App.vue
@@ -1,12 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {listen} from "@tauri-apps/api/event";
|
|
||||||
import {onMounted, onUnmounted, ref} from 'vue';
|
import {onMounted, onUnmounted, ref} from 'vue';
|
||||||
import {NavigationMenuItem} from "@nuxt/ui";
|
import {NavigationMenuItem} from "@nuxt/ui";
|
||||||
|
import {createApiClient} from "./api";
|
||||||
|
import {useConfigStore, useServerStore} from "./stores";
|
||||||
|
|
||||||
|
// stores
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const serverStore = useServerStore();
|
||||||
|
|
||||||
// vars
|
// vars
|
||||||
let unlisten: (() => void) | null = null;
|
let unlisten: (() => void) | null = null;
|
||||||
|
const apiClient = createApiClient('http://localhost:7000'); // TODO: récupérer de la config
|
||||||
const servers = ref<NavigationMenuItem[]>([
|
const servers = ref<NavigationMenuItem[]>([
|
||||||
{ label: 'Main Canal', to: '/', icon: 'i-lucide-house'},
|
{label: 'Main Canal', to: '/', icon: 'i-lucide-house'},
|
||||||
|
{label: 'Config', to: '/config', icon: 'i-lucide-settings'}
|
||||||
])
|
])
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
@@ -20,18 +27,17 @@ const getInitials = (name: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchServers() {
|
async function fetchServers() {
|
||||||
// todo : Cette méthode devra appeler la liste des serveurs via le "proxy" rust
|
try {
|
||||||
const response = await fetch("http://localhost:7000/api/server/servers/")
|
const data = await apiClient.getServerList();
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
let data = await response.json()
|
|
||||||
data.forEach(server => {
|
data.forEach(server => {
|
||||||
servers.value.push({
|
servers.value.push({
|
||||||
label: server.name,
|
label: server.name,
|
||||||
to: `/server/${server.id}`, avatar: {text: getInitials(server.name)}
|
to: `/server/${server.id}`, avatar: {text: getInitials(server.name)}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch servers", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -42,10 +48,8 @@ async function fetchServers() {
|
|||||||
// ])
|
// ])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// tauri
|
// config
|
||||||
// unlisten = await listen("app-ready", event => {
|
await configStore.init();
|
||||||
// console.log("App is ready", event)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// fetch servers
|
// fetch servers
|
||||||
await fetchServers()
|
await fetchServers()
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface IApiClient {
|
|||||||
// AUTH
|
// AUTH
|
||||||
login(req: LoginRequest): Promise<LoginResponse>;
|
login(req: LoginRequest): Promise<LoginResponse>;
|
||||||
|
|
||||||
|
verifyToken(): Promise<UserResponse>;
|
||||||
|
|
||||||
claimAdmin(req: ClaimAdminRequest): Promise<void>;
|
claimAdmin(req: ClaimAdminRequest): Promise<void>;
|
||||||
|
|
||||||
sshChallenge(): Promise<SshChallengeResponse>;
|
sshChallenge(): Promise<SshChallengeResponse>;
|
||||||
@@ -33,6 +35,8 @@ export interface IApiClient {
|
|||||||
|
|
||||||
createServer(req: CreateServerRequest): Promise<ServerResponse>;
|
createServer(req: CreateServerRequest): Promise<ServerResponse>;
|
||||||
|
|
||||||
|
getServerTree(serverId: string): Promise<any>;
|
||||||
|
|
||||||
// CATEGORY
|
// CATEGORY
|
||||||
getCategoryList(serverId?: string): Promise<CategoryResponse[]>;
|
getCategoryList(serverId?: string): Promise<CategoryResponse[]>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './client';
|
export * from './client';
|
||||||
export * from './tauri-client';
|
export * from './tauri-client';
|
||||||
|
export * from './web-client';
|
||||||
|
|
||||||
import {TauriApiClient} from './tauri-client';
|
import {TauriApiClient} from './tauri-client';
|
||||||
|
import {WebApiClient} from './web-client';
|
||||||
import type {IApiClient} from './client';
|
import type {IApiClient} from './client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Usine pour créer des clients API.
|
* Usine pour créer des clients API.
|
||||||
* Plus tard, cette usine pourra retourner une implémentation Web ou Tauri
|
* Elle retourne une implémentation Web ou Tauri selon l'environnement.
|
||||||
* selon l'environnement (isTauri).
|
|
||||||
*/
|
*/
|
||||||
export function createApiClient(baseUrl: string): IApiClient {
|
export function createApiClient(baseUrl: string): IApiClient {
|
||||||
// Pour le moment, on retourne systématiquement l'implémentation Tauri.
|
if ((window as any).__TAURI_INTERNALS__) {
|
||||||
return new TauriApiClient(baseUrl);
|
return new TauriApiClient(baseUrl);
|
||||||
|
}
|
||||||
|
return new WebApiClient(baseUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export class TauriApiClient implements IApiClient {
|
|||||||
return await invoke<LoginResponse>('api_login', {baseUrl: this.baseUrl, req});
|
return await invoke<LoginResponse>('api_login', {baseUrl: this.baseUrl, req});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async verifyToken(): Promise<UserResponse> {
|
||||||
|
return await invoke<UserResponse>('api_verify_token', {baseUrl: this.baseUrl});
|
||||||
|
}
|
||||||
|
|
||||||
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
||||||
return await invoke<void>('api_claim_admin', {baseUrl: this.baseUrl, req});
|
return await invoke<void>('api_claim_admin', {baseUrl: this.baseUrl, req});
|
||||||
}
|
}
|
||||||
@@ -46,6 +50,10 @@ export class TauriApiClient implements IApiClient {
|
|||||||
return await invoke<ServerResponse>('api_server_create', {baseUrl: this.baseUrl, req});
|
return await invoke<ServerResponse>('api_server_create', {baseUrl: this.baseUrl, req});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getServerTree(serverId: string): Promise<any> {
|
||||||
|
return await invoke<any>('api_server_tree', {baseUrl: this.baseUrl, serverId});
|
||||||
|
}
|
||||||
|
|
||||||
// CATEGORY
|
// CATEGORY
|
||||||
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
||||||
return await invoke<CategoryResponse[]>('api_category_list', {
|
return await invoke<CategoryResponse[]>('api_category_list', {
|
||||||
|
|||||||
135
src/api/web-client.ts
Normal file
135
src/api/web-client.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import type {IApiClient} from './client';
|
||||||
|
import type {
|
||||||
|
CategoryResponse,
|
||||||
|
ChannelResponse,
|
||||||
|
ClaimAdminRequest,
|
||||||
|
CreateCategoryRequest,
|
||||||
|
CreateChannelRequest,
|
||||||
|
CreateMessageRequest,
|
||||||
|
CreateServerRequest,
|
||||||
|
LoginRequest,
|
||||||
|
LoginResponse,
|
||||||
|
MessageResponse,
|
||||||
|
ServerResponse,
|
||||||
|
SshChallengeResponse,
|
||||||
|
UserResponse,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation Web (standard) de l'interface API.
|
||||||
|
* Elle utilise fetch() pour communiquer directement avec le serveur.
|
||||||
|
*/
|
||||||
|
export class WebApiClient implements IApiClient {
|
||||||
|
private token: string | null = null;
|
||||||
|
|
||||||
|
constructor(public readonly baseUrl: string) {
|
||||||
|
this.token = localStorage.getItem(`token_${baseUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(method: string, path: string, body?: any): Promise<T> {
|
||||||
|
// Nettoyer l'URL
|
||||||
|
const cleanBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||||
|
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||||
|
const url = `${cleanBaseUrl}/${cleanPath}`;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.token) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorData;
|
||||||
|
try {
|
||||||
|
errorData = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
errorData = {message: await response.text()};
|
||||||
|
}
|
||||||
|
throw new Error(errorData.message || `Erreur HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return {} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setToken(token: string) {
|
||||||
|
this.token = token;
|
||||||
|
localStorage.setItem(`token_${this.baseUrl}`, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUTH
|
||||||
|
async login(req: LoginRequest): Promise<LoginResponse> {
|
||||||
|
const res = await this.request<LoginResponse>('POST', 'api/auth/login', req);
|
||||||
|
this.setToken(res.token);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyToken(): Promise<UserResponse> {
|
||||||
|
return await this.request<UserResponse>('GET', 'api/auth/me');
|
||||||
|
}
|
||||||
|
|
||||||
|
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
||||||
|
return await this.request<void>('POST', 'api/auth/claim-admin', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sshChallenge(): Promise<SshChallengeResponse> {
|
||||||
|
return await this.request<SshChallengeResponse>('POST', 'api/auth/ssh-challenge');
|
||||||
|
}
|
||||||
|
|
||||||
|
// SERVER
|
||||||
|
async getServerList(): Promise<ServerResponse[]> {
|
||||||
|
return await this.request<ServerResponse[]>('GET', 'api/server');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
||||||
|
return await this.request<ServerResponse>('POST', 'api/server', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getServerTree(serverId: string): Promise<any> {
|
||||||
|
return await this.request<any>('GET', `api/server/servers/${serverId}/tree/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CATEGORY
|
||||||
|
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
||||||
|
const path = serverId ? `api/category?server_id=${serverId}` : 'api/category';
|
||||||
|
return await this.request<CategoryResponse[]>('GET', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
||||||
|
return await this.request<CategoryResponse>('POST', 'api/category', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHANNEL
|
||||||
|
async getChannelList(): Promise<ChannelResponse[]> {
|
||||||
|
return await this.request<ChannelResponse[]>('GET', 'api/channel');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
||||||
|
return await this.request<ChannelResponse>('POST', 'api/channel', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MESSAGE
|
||||||
|
async getMessageList(): Promise<MessageResponse[]> {
|
||||||
|
return await this.request<MessageResponse[]>('GET', 'api/message');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
||||||
|
return await this.request<MessageResponse>('POST', 'api/message', req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// USER
|
||||||
|
async getUserList(): Promise<UserResponse[]> {
|
||||||
|
return await this.request<UserResponse[]>('GET', 'api/user');
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.ts
10
src/main.ts
@@ -1,15 +1,17 @@
|
|||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import {createApp} from 'vue'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import {createRouter, createWebHistory} from 'vue-router'
|
||||||
import ui from '@nuxt/ui/vue-plugin'
|
import ui from '@nuxt/ui/vue-plugin'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import {createPinia} from "pinia";
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: () => import('./pages/index.vue') },
|
{path: '/', component: () => import('./pages/index.vue')},
|
||||||
|
{path: '/config', component: () => import('./pages/config.vue')},
|
||||||
{
|
{
|
||||||
path: '/server/:server_id',
|
path: '/server/:server_id',
|
||||||
component: () => import('./pages/server_detail.vue'),
|
component: () => import('./pages/server_detail.vue'),
|
||||||
@@ -28,5 +30,5 @@ const router = createRouter({
|
|||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ui)
|
app.use(ui)
|
||||||
|
app.use(createPinia())
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
240
src/pages/config.vue
Normal file
240
src/pages/config.vue
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {onMounted, ref} from 'vue'
|
||||||
|
import {createConfigClient} from '../config'
|
||||||
|
|
||||||
|
// --- Types ---
|
||||||
|
// Ils sont maintenant importés ou définis via l'interface du client
|
||||||
|
|
||||||
|
const configClient = createConfigClient()
|
||||||
|
const config = ref<any>({
|
||||||
|
servers: [],
|
||||||
|
identities: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const items = [{
|
||||||
|
label: 'Serveurs',
|
||||||
|
icon: 'i-lucide-server',
|
||||||
|
slot: 'servers'
|
||||||
|
}, {
|
||||||
|
label: 'Identités',
|
||||||
|
icon: 'i-lucide-user',
|
||||||
|
slot: 'identities'
|
||||||
|
}]
|
||||||
|
|
||||||
|
// --- Methods ---
|
||||||
|
|
||||||
|
async function fetchConfig() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await configClient.get()
|
||||||
|
config.value = res
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la récupération de la config:', error)
|
||||||
|
toast.add({title: 'Erreur', description: 'Impossible de charger la configuration.', color: 'red'})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await configClient.update(config.value)
|
||||||
|
// Re-fetch config to get any auto-generated keys
|
||||||
|
await fetchConfig()
|
||||||
|
toast.add({title: 'Succès', description: 'Configuration sauvegardée.', color: 'green'})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la sauvegarde de la config:', error)
|
||||||
|
toast.add({title: 'Erreur', description: 'Impossible de sauvegarder la configuration.', color: 'red'})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async function generateKey(index: number) {
|
||||||
|
// try {
|
||||||
|
// // Note: generate_ssh_key n'est pas encore dans IConfigClient, on peut l'ajouter si besoin
|
||||||
|
// // ou garder un invoke direct si c'est spécifique à Tauri, mais ici on veut de l'abstrait.
|
||||||
|
// // Pour le moment on utilise invoke car c'est un utilitaire.
|
||||||
|
// const {invoke} from '@tauri-apps/api/core'
|
||||||
|
// const key = await invoke<string>('generate_ssh_key')
|
||||||
|
// config.value.identities[index].private_key = key
|
||||||
|
// toast.add({title: 'Succès', description: 'Clé SSH générée.', color: 'green'})
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Erreur lors de la génération de la clé:', error)
|
||||||
|
// toast.add({title: 'Erreur', description: 'Impossible de générer la clé.', color: 'red'})
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
function addServer() {
|
||||||
|
config.value.servers.push({adresse: '', identity: ''})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeServer(index: number) {
|
||||||
|
config.value.servers.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIdentity() {
|
||||||
|
config.value.identities.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
username: '',
|
||||||
|
private_key: '',
|
||||||
|
mode: 'private_key_path'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeIdentity(index: number) {
|
||||||
|
config.value.identities.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchConfig()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6 max-w-4xl mx-auto overflow-y-auto h-full">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h1 class="text-2xl font-bold">Configuration</h1>
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-save"
|
||||||
|
color="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="saveConfig"
|
||||||
|
>
|
||||||
|
Sauvegarder
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UTabs :items="items" class="w-full">
|
||||||
|
<template #servers>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<div v-for="(server, index) in config.servers" :key="index"
|
||||||
|
class="p-4 border border-gray-200 dark:border-gray-800 rounded-lg relative bg-white dark:bg-gray-900 shadow-sm">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<UFormField label="Adresse du serveur (IP:Port)">
|
||||||
|
<UInput v-model="server.adresse" placeholder="ex: 127.0.0.1:50051" class="w-full"/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Identité à utiliser">
|
||||||
|
<USelect
|
||||||
|
v-model="server.identity"
|
||||||
|
:items="config.identities.map(i => ({ label: i.username || i.id, value: i.id }))"
|
||||||
|
placeholder="Sélectionner une identité"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
color="red"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="absolute top-2 right-2"
|
||||||
|
@click="removeServer(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
variant="dashed"
|
||||||
|
block
|
||||||
|
@click="addServer"
|
||||||
|
>
|
||||||
|
Ajouter un serveur
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #identities>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<div v-for="(identity, index) in config.identities" :key="identity.id"
|
||||||
|
class="p-4 border border-gray-200 dark:border-gray-800 rounded-lg relative bg-white dark:bg-gray-900 shadow-sm">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<UFormField label="ID d'identité (unique)">
|
||||||
|
<UInput v-model="identity.id" placeholder="ex: mon-id" class="w-full"/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Nom d'utilisateur">
|
||||||
|
<UInput v-model="identity.username" placeholder="ex: MonPseudo" class="w-full"/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Mode d'authentification" class="md:col-span-2">
|
||||||
|
<USelect
|
||||||
|
v-model="identity.mode"
|
||||||
|
:items="[
|
||||||
|
{ label: 'Clé (Fichier)', value: 'private_key_path' },
|
||||||
|
{ label: 'Clé (Base64)', value: 'private_key_base64' },
|
||||||
|
{ label: 'Login uniquement', value: 'login' }
|
||||||
|
]"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<template v-if="identity.mode === 'private_key_path' || identity.mode === 'private_key_base64'">
|
||||||
|
<UFormField :label="identity.mode === 'private_key_path' ? 'Chemin de la clé' : 'Clé privée (Base64)'"
|
||||||
|
class="md:col-span-2">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UTextarea
|
||||||
|
v-model="identity.private_key"
|
||||||
|
:placeholder="identity.mode === 'private_key_path' ? 'ex: ~/.ssh/id_rsa' : 'Entrez la clé encodée en base64 (Si vide, une clé sera générée au besoin)'"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<!-- <UButton-->
|
||||||
|
<!-- v-if="identity.mode === 'private_key_base64'"-->
|
||||||
|
<!-- icon="i-lucide-key"-->
|
||||||
|
<!-- variant="subtle"-->
|
||||||
|
<!-- size="xs"-->
|
||||||
|
<!-- label="Générer une clé SSH"-->
|
||||||
|
<!-- @click="generateKey(index)"-->
|
||||||
|
<!-- />-->
|
||||||
|
</div>
|
||||||
|
</UFormField>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<template v-if="identity.token">
|
||||||
|
<UFormField label="Jeton JWT (Persistant)" class="md:col-span-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UInput v-model="identity.token" readonly class="flex-1 font-mono text-xs"/>
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-copy"
|
||||||
|
variant="ghost"
|
||||||
|
color="neutral"
|
||||||
|
@click="() => {
|
||||||
|
navigator.clipboard.writeText(identity.token || '')
|
||||||
|
toast.add({ title: 'Copié', description: 'Jeton JWT copié dans le presse-papier', color: 'green' })
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UFormField>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
color="red"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="absolute top-2 right-2"
|
||||||
|
@click="removeIdentity(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-plus"
|
||||||
|
variant="dashed"
|
||||||
|
block
|
||||||
|
@click="addIdentity"
|
||||||
|
>
|
||||||
|
Ajouter une identité
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, onMounted} from 'vue';
|
import {onMounted, ref} from 'vue';
|
||||||
import {NavigationMenuItem} from "@nuxt/ui";
|
import {NavigationMenuItem} from "@nuxt/ui";
|
||||||
|
import {createApiClient} from "../api";
|
||||||
|
|
||||||
// vars
|
// vars
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server_id: string
|
server_id: string
|
||||||
}>()
|
}>()
|
||||||
const channels = ref<NavigationMenuItem[]>([])
|
const channels = ref<NavigationMenuItem[]>([])
|
||||||
|
const apiClient = createApiClient('http://localhost:7000'); // TODO: récupérer de la config
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
async function fetchTree(){
|
async function fetchTree() {
|
||||||
const response = await fetch(`http://localhost:7000/api/server/servers/${props.server_id}/tree/`)
|
try {
|
||||||
let data = await response.json()
|
const data = await apiClient.getServerTree(props.server_id);
|
||||||
|
|
||||||
const formattedChannels: any[] = []
|
const formattedChannels: any[] = []
|
||||||
|
|
||||||
data.forEach(el => {
|
data.forEach((el: any) => {
|
||||||
if (el.type === "category") {
|
if (el.type === "category") {
|
||||||
// On crée un groupe pour la catégorie
|
// On crée un groupe pour la catégorie
|
||||||
const categoryGroup = {
|
const categoryGroup = {
|
||||||
@@ -39,6 +41,9 @@ async function fetchTree(){
|
|||||||
})
|
})
|
||||||
|
|
||||||
channels.value = formattedChannels
|
channels.value = formattedChannels
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch tree", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const channels = ref<NavigationMenuItem[]>([
|
// const channels = ref<NavigationMenuItem[]>([
|
||||||
@@ -61,7 +66,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardSidebar>
|
</UDashboardSidebar>
|
||||||
<router-view />
|
<router-view/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
39
src/stores/config.ts
Normal file
39
src/stores/config.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {type ConfigTree, createConfigClient} from '../config';
|
||||||
|
|
||||||
|
export const useConfigStore = defineStore('config', () => {
|
||||||
|
const client = createConfigClient();
|
||||||
|
const config = ref<ConfigTree | null>(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
config.value = await client.get();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateConfig(newConfig: ConfigTree) {
|
||||||
|
await client.update(newConfig);
|
||||||
|
config.value = newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialiser la configuration et écouter les changements
|
||||||
|
async function init() {
|
||||||
|
await loadConfig();
|
||||||
|
await client.onChanged((updatedConfig) => {
|
||||||
|
config.value = updatedConfig;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
loading,
|
||||||
|
loadConfig,
|
||||||
|
updateConfig,
|
||||||
|
init,
|
||||||
|
};
|
||||||
|
});
|
||||||
22
src/stores/global.ts
Normal file
22
src/stores/global.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {ref} from 'vue';
|
||||||
|
|
||||||
|
export const useGlobalStore = defineStore('global', () => {
|
||||||
|
const loading = ref(false);
|
||||||
|
const theme = ref('dark');
|
||||||
|
|
||||||
|
function setLoading(value: boolean) {
|
||||||
|
loading.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
theme.value = theme.value === 'dark' ? 'light' : 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
theme,
|
||||||
|
setLoading,
|
||||||
|
toggleTheme,
|
||||||
|
};
|
||||||
|
});
|
||||||
5
src/stores/index.ts
Normal file
5
src/stores/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './global';
|
||||||
|
export * from './config';
|
||||||
|
export * from './server_manager';
|
||||||
|
export * from './server_context';
|
||||||
|
export * from './server';
|
||||||
29
src/stores/server.ts
Normal file
29
src/stores/server.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
import {useConfigStore} from './config';
|
||||||
|
import {useServerManagerStore} from './server_manager';
|
||||||
|
import {defineServerContextStore} from './server_context';
|
||||||
|
|
||||||
|
export const useServerStore = defineStore('server', () => {
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const managerStore = useServerManagerStore();
|
||||||
|
|
||||||
|
const currentServerId = ref<string | null>(null);
|
||||||
|
|
||||||
|
// Getter pour obtenir le contexte du serveur actuel
|
||||||
|
const currentContext = computed(() => {
|
||||||
|
if (!currentServerId.value) return null;
|
||||||
|
return defineServerContextStore(currentServerId.value)();
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectServer(serverId: string) {
|
||||||
|
currentServerId.value = serverId;
|
||||||
|
managerStore.addServer(serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentServerId,
|
||||||
|
currentContext,
|
||||||
|
selectServer,
|
||||||
|
};
|
||||||
|
});
|
||||||
68
src/stores/server_context.ts
Normal file
68
src/stores/server_context.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {ref} from 'vue';
|
||||||
|
import {createApiClient} from '../api';
|
||||||
|
|
||||||
|
export interface ServerState {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
isConnected: boolean;
|
||||||
|
isConnecting: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nous utilisons une fonction pour générer une définition de store unique par serveur
|
||||||
|
export const defineServerContextStore = (serverId: string) => {
|
||||||
|
return defineStore(`server_context:${serverId}`, () => {
|
||||||
|
const state = ref<ServerState>({
|
||||||
|
id: serverId,
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
isConnected: false,
|
||||||
|
isConnecting: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiClient = ref<ReturnType<typeof createApiClient> | null>(null);
|
||||||
|
const subServers = ref<any[]>([]); // Liste des sous-serveurs virtuels
|
||||||
|
|
||||||
|
function init(name: string, address: string) {
|
||||||
|
state.value.name = name;
|
||||||
|
state.value.address = address;
|
||||||
|
apiClient.value = createApiClient(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConnectionState(connected: boolean) {
|
||||||
|
state.value.isConnected = connected;
|
||||||
|
state.value.isConnecting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConnecting(connecting: boolean) {
|
||||||
|
state.value.isConnecting = connecting;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setError(err: string | null) {
|
||||||
|
state.value.error = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSubServers() {
|
||||||
|
if (!apiClient.value) return;
|
||||||
|
try {
|
||||||
|
const servers = await apiClient.value.getServerList();
|
||||||
|
subServers.value = servers;
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || "Failed to fetch sub-servers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
subServers,
|
||||||
|
init,
|
||||||
|
setConnectionState,
|
||||||
|
setConnecting,
|
||||||
|
setError,
|
||||||
|
fetchSubServers,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
29
src/stores/server_manager.ts
Normal file
29
src/stores/server_manager.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {defineStore} from 'pinia';
|
||||||
|
import {ref} from 'vue';
|
||||||
|
|
||||||
|
export const useServerManagerStore = defineStore('server_manager', () => {
|
||||||
|
// Liste des IDs de serveurs actuellement chargés/connectés
|
||||||
|
const connectedServerIds = ref<string[]>([]);
|
||||||
|
|
||||||
|
// Pour obtenir un contexte spécifique, l'utilisateur du store appellera defineServerContextStore(id)()
|
||||||
|
// Le manager peut aider à orchestrer ces connexions.
|
||||||
|
|
||||||
|
function addServer(serverId: string) {
|
||||||
|
if (!connectedServerIds.value.includes(serverId)) {
|
||||||
|
connectedServerIds.value.push(serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeServer(serverId: string) {
|
||||||
|
const index = connectedServerIds.value.indexOf(serverId);
|
||||||
|
if (index !== -1) {
|
||||||
|
connectedServerIds.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectedServerIds,
|
||||||
|
addServer,
|
||||||
|
removeServer,
|
||||||
|
};
|
||||||
|
});
|
||||||
55
src/ws/README.md
Normal file
55
src/ws/README.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Système WebSocket OxSpeak
|
||||||
|
|
||||||
|
Ce module fournit une interface WebSocket générique pour communiquer avec les serveurs OxSpeak.
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
Le système utilise une interface générique `IWsClient` qui supporte automatiquement deux environnements :
|
||||||
|
|
||||||
|
- **Tauri** : La connexion est gérée par le backend Rust (meilleure stabilité, reconnexion automatique gérée en Rust).
|
||||||
|
- **Web** : Utilise l'API standard `WebSocket` du navigateur.
|
||||||
|
|
||||||
|
### Exemple de base
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createWsClient } from '@/ws';
|
||||||
|
|
||||||
|
const wsClient = createWsClient();
|
||||||
|
|
||||||
|
async function startChat(serverUrl: string, token: string) {
|
||||||
|
// 1. Connexion
|
||||||
|
await wsClient.connect(serverUrl, token);
|
||||||
|
|
||||||
|
// 2. Écoute des messages
|
||||||
|
const unsubscribe = wsClient.onMessage((payload) => {
|
||||||
|
console.log('Nouveau message reçu:', payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Envoi d'un message
|
||||||
|
await wsClient.send({
|
||||||
|
type: 'chat_message',
|
||||||
|
content: 'Bonjour tout le monde !'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plus tard pour se déconnecter
|
||||||
|
// await wsClient.disconnect();
|
||||||
|
// unsubscribe();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avantages de l'implémentation Tauri
|
||||||
|
|
||||||
|
L'implémentation Tauri via Rust offre plusieurs avantages :
|
||||||
|
|
||||||
|
1. **Reconnexion automatique** : Le backend Rust tente de se reconnecter toutes les 5 secondes en cas de perte de
|
||||||
|
connexion.
|
||||||
|
2. **Persistence** : La connexion survit aux rechargements de la page frontend (HMR ou F5).
|
||||||
|
3. **Multi-serveur** : Le `WsManager` en Rust peut maintenir plusieurs connexions actives simultanément pour différents
|
||||||
|
serveurs.
|
||||||
|
|
||||||
|
## Structure du Backend (Rust)
|
||||||
|
|
||||||
|
- `WsClient` : Gère la boucle de connexion, lecture et écriture pour un serveur donné.
|
||||||
|
- `WsManager` : Gère le dictionnaire des connexions actives.
|
||||||
|
- `commands.rs` : Expose les fonctions `ws_connect`, `ws_send` et `ws_disconnect` à Tauri.
|
||||||
|
- Événement `ws-message` : Émis par Rust vers le Frontend pour chaque message reçu.
|
||||||
113
src/ws/client.ts
Normal file
113
src/ws/client.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import {invoke} from '@tauri-apps/api/core';
|
||||||
|
import {listen} from '@tauri-apps/api/event';
|
||||||
|
import type {IWsClient, WsEvent} from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation Tauri du client WebSocket.
|
||||||
|
* Utilise le backend Rust pour maintenir la connexion.
|
||||||
|
*/
|
||||||
|
export class TauriWsClient implements IWsClient {
|
||||||
|
private serverUrl: string | null = null;
|
||||||
|
|
||||||
|
async connect(serverUrl: string, token?: string): Promise<void> {
|
||||||
|
this.serverUrl = serverUrl;
|
||||||
|
await invoke('ws_connect', {serverUrl, token});
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(message: any): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<WsEvent>('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 {
|
||||||
|
private ws: WebSocket | null = null;
|
||||||
|
private messageCallbacks: Set<(payload: any) => void> = new Set();
|
||||||
|
private serverUrl: string | null = null;
|
||||||
|
|
||||||
|
async connect(serverUrl: string, token?: string): Promise<void> {
|
||||||
|
this.serverUrl = serverUrl;
|
||||||
|
let wsUrl = serverUrl.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||||
|
if (!wsUrl.endsWith('/ws')) {
|
||||||
|
wsUrl = `${wsUrl.replace(/\/$/, '')}/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
wsUrl += (wsUrl.includes('?') ? '&' : '?') + `token=${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(message: any): Promise<void> {
|
||||||
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error('WebSocket non ouvert');
|
||||||
|
}
|
||||||
|
const msgString = typeof message === 'string' ? message : JSON.stringify(message);
|
||||||
|
this.ws.send(msgString);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect(): Promise<void> {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(callback: (payload: any) => void): () => void {
|
||||||
|
this.messageCallbacks.add(callback);
|
||||||
|
return () => this.messageCallbacks.delete(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory pour créer le client approprié.
|
||||||
|
*/
|
||||||
|
export function createWsClient(): IWsClient {
|
||||||
|
if ((window as any).__TAURI_INTERNALS__) {
|
||||||
|
return new TauriWsClient();
|
||||||
|
}
|
||||||
|
return new WebWsClient();
|
||||||
|
}
|
||||||
2
src/ws/index.ts
Normal file
2
src/ws/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './client';
|
||||||
27
src/ws/types.ts
Normal file
27
src/ws/types.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export interface WsEvent {
|
||||||
|
server_url: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWsClient {
|
||||||
|
/**
|
||||||
|
* Se connecte au serveur WebSocket.
|
||||||
|
*/
|
||||||
|
connect(serverUrl: string, token?: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un message au serveur.
|
||||||
|
*/
|
||||||
|
send(message: any): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Se déconnecte du serveur.
|
||||||
|
*/
|
||||||
|
disconnect(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S'abonne aux messages entrants.
|
||||||
|
* Retourne une fonction pour se désabonner.
|
||||||
|
*/
|
||||||
|
onMessage(callback: (payload: any) => void): () => void;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user