init
This commit is contained in:
1
resources/openapi.json
Normal file
1
resources/openapi.json
Normal file
File diff suppressed because one or more lines are too long
1217
src-tauri/Cargo.lock
generated
1217
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,15 +18,17 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri = { version = "2.10.3", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
opusic-sys = "0.5"
|
||||
tokio = { version = "1.49", features = ["full"] }
|
||||
opusic-sys = "0.6"
|
||||
tokio = { version = "1.50", features = ["full"] }
|
||||
reqwest = { version = "0.13", features = ["json", "rustls"] }
|
||||
thiserror = "2.0"
|
||||
|
||||
figment = "0.10.19"
|
||||
parking_lot = "0.12"
|
||||
toml = "0.9"
|
||||
ssh-key = { version = "0.6", features = ["default", "crypto"] }
|
||||
base64 = "0.22"
|
||||
toml = "1.0.7+spec-1.1.0"
|
||||
ssh-key = { version = "0.7.0-rc.9", features = ["default", "crypto"] }
|
||||
base64 = "0.22"
|
||||
|
||||
247
src-tauri/src/api/client.rs
Normal file
247
src-tauri/src/api/client.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use crate::api::models::*;
|
||||
use parking_lot::RwLock;
|
||||
use reqwest::{header, Client, Method, RequestBuilder, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Erreurs possibles lors de l'utilisation du client API.
|
||||
#[derive(Debug, thiserror::Error, Serialize)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
pub enum ApiError {
|
||||
#[error("Erreur de requête HTTP: {0}")]
|
||||
Http(String),
|
||||
#[error("Erreur de désérialisation: {0}")]
|
||||
Json(String),
|
||||
#[error("Réponse d'erreur du serveur: {status} - {body}")]
|
||||
ServerError { status: u16, body: String },
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for ApiError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
ApiError::Http(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ApiError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ApiError::Json(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type ApiResult<T> = Result<T, ApiError>;
|
||||
|
||||
/// Un client API générique capable de se connecter à plusieurs serveurs.
|
||||
/// Il gère l'authentification par token JWT de manière thread-safe.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiClient {
|
||||
base_url: String,
|
||||
inner: Client,
|
||||
token: Arc<RwLock<Option<String>>>,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
/// Initialise un nouveau client pour un serveur spécifique.
|
||||
pub fn new(base_url: String) -> Self {
|
||||
let mut headers = header::HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
header::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
|
||||
let inner = Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new());
|
||||
|
||||
Self {
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
inner,
|
||||
token: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Définit le token JWT à utiliser pour les requêtes authentifiées.
|
||||
pub fn set_token(&self, token: String) {
|
||||
let mut lock = self.token.write();
|
||||
*lock = Some(token);
|
||||
}
|
||||
|
||||
/// Récupère le token JWT actuel s'il existe.
|
||||
pub fn get_token(&self) -> Option<String> {
|
||||
self.token.read().clone()
|
||||
}
|
||||
|
||||
/// Supprime le token JWT actuel.
|
||||
pub fn clear_token(&self) {
|
||||
let mut lock = self.token.write();
|
||||
*lock = None;
|
||||
}
|
||||
|
||||
/// Construit une requête HTTP avec l'authentification si disponible.
|
||||
fn build_request(&self, method: Method, path: &str) -> RequestBuilder {
|
||||
let url = if path.starts_with("http") {
|
||||
path.to_string()
|
||||
} else {
|
||||
format!("{}/{}", self.base_url, path.trim_start_matches('/'))
|
||||
};
|
||||
let mut rb = self.inner.request(method, url);
|
||||
|
||||
if let Some(token) = self.token.read().as_ref() {
|
||||
rb = rb.header(header::AUTHORIZATION, format!("Bearer {}", token));
|
||||
}
|
||||
|
||||
rb
|
||||
}
|
||||
|
||||
async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> ApiResult<T> {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
let data = response.json::<T>().await?;
|
||||
Ok(data)
|
||||
} else {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
Err(ApiError::ServerError {
|
||||
status: status.as_u16(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
|
||||
let response = self.build_request(Method::GET, path).send().await?;
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn post<B: Serialize, T: DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
) -> ApiResult<T> {
|
||||
let response = self
|
||||
.build_request(Method::POST, path)
|
||||
.json(body)
|
||||
.send()
|
||||
.await?;
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn post_empty<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
|
||||
let response = self.build_request(Method::POST, path).send().await?;
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn put<B: Serialize, T: DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
) -> ApiResult<T> {
|
||||
let response = self
|
||||
.build_request(Method::PUT, path)
|
||||
.json(body)
|
||||
.send()
|
||||
.await?;
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> ApiResult<T> {
|
||||
let response = self.build_request(Method::DELETE, path).send().await?;
|
||||
self.handle_response(response).await
|
||||
}
|
||||
|
||||
// --- ECOSYSTEMS ---
|
||||
|
||||
// Auth
|
||||
pub async fn login(&self, req: &LoginRequest) -> ApiResult<LoginResponse> {
|
||||
self.post("api/auth/login", req).await
|
||||
}
|
||||
|
||||
pub async fn claim_admin(&self, req: &ClaimAdminRequest) -> ApiResult<()> {
|
||||
self.post("api/auth/claim-admin", req).await
|
||||
}
|
||||
|
||||
pub async fn ssh_challenge(&self) -> ApiResult<SshChallengeResponse> {
|
||||
self.post_empty("api/auth/ssh-challenge").await
|
||||
}
|
||||
|
||||
// Server
|
||||
pub async fn server_list(&self) -> ApiResult<Vec<ServerResponse>> {
|
||||
self.get("api/server").await
|
||||
}
|
||||
|
||||
pub async fn server_create(&self, req: &CreateServerRequest) -> ApiResult<ServerResponse> {
|
||||
self.post("api/server", req).await
|
||||
}
|
||||
|
||||
// Category
|
||||
pub async fn category_list(&self, server_id: Option<&str>) -> ApiResult<Vec<CategoryResponse>> {
|
||||
let path = if let Some(id) = server_id {
|
||||
format!("api/category?server_id={}", id)
|
||||
} else {
|
||||
"api/category".to_string()
|
||||
};
|
||||
self.get(&path).await
|
||||
}
|
||||
|
||||
pub async fn category_create(
|
||||
&self,
|
||||
req: &CreateCategoryRequest,
|
||||
) -> ApiResult<CategoryResponse> {
|
||||
self.post("api/category", req).await
|
||||
}
|
||||
|
||||
pub async fn category_detail(&self, id: &str) -> ApiResult<CategoryResponse> {
|
||||
self.get(&format!("api/category/{}", id)).await
|
||||
}
|
||||
|
||||
pub async fn category_update(
|
||||
&self,
|
||||
id: &str,
|
||||
req: &CreateCategoryRequest,
|
||||
) -> ApiResult<CategoryResponse> {
|
||||
self.put(&format!("api/category/{}", id), req).await
|
||||
}
|
||||
|
||||
pub async fn category_delete(&self, id: &str) -> ApiResult<()> {
|
||||
self.delete(&format!("api/category/{}", id)).await
|
||||
}
|
||||
|
||||
// Channel
|
||||
pub async fn channel_list(&self) -> ApiResult<Vec<ChannelResponse>> {
|
||||
self.get("api/channel").await
|
||||
}
|
||||
|
||||
pub async fn channel_create(&self, req: &CreateChannelRequest) -> ApiResult<ChannelResponse> {
|
||||
self.post("api/channel", req).await
|
||||
}
|
||||
|
||||
pub async fn channel_detail(&self, id: &str) -> ApiResult<ChannelResponse> {
|
||||
self.get(&format!("api/channel/{}", id)).await
|
||||
}
|
||||
|
||||
pub async fn channel_update(
|
||||
&self,
|
||||
id: &str,
|
||||
req: &CreateChannelRequest,
|
||||
) -> ApiResult<ChannelResponse> {
|
||||
self.put(&format!("api/channel/{}", id), req).await
|
||||
}
|
||||
|
||||
pub async fn channel_delete(&self, id: &str) -> ApiResult<()> {
|
||||
self.delete(&format!("api/channel/{}", id)).await
|
||||
}
|
||||
|
||||
// Message
|
||||
pub async fn message_list(&self) -> ApiResult<Vec<MessageResponse>> {
|
||||
self.get("api/message").await
|
||||
}
|
||||
|
||||
pub async fn message_create(&self, req: &CreateMessageRequest) -> ApiResult<MessageResponse> {
|
||||
self.post("api/message", req).await
|
||||
}
|
||||
|
||||
// User
|
||||
pub async fn user_list(&self) -> ApiResult<Vec<UserResponse>> {
|
||||
self.get("api/user").await
|
||||
}
|
||||
}
|
||||
133
src-tauri/src/api/commands.rs
Normal file
133
src-tauri/src/api/commands.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use crate::api::models::*;
|
||||
use crate::api::ApiResult;
|
||||
use crate::app::state::AppState;
|
||||
use tauri::{command, State};
|
||||
|
||||
// --- AUTH ---
|
||||
|
||||
#[command]
|
||||
pub async fn api_login(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
req: LoginRequest,
|
||||
) -> ApiResult<LoginResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
let res = client.login(&req).await?;
|
||||
client.set_token(res.token.clone());
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_claim_admin(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
req: ClaimAdminRequest,
|
||||
) -> ApiResult<()> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.claim_admin(&req).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_ssh_challenge(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<SshChallengeResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.ssh_challenge().await
|
||||
}
|
||||
|
||||
// --- SERVER ---
|
||||
|
||||
#[command]
|
||||
pub async fn api_server_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<Vec<ServerResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.server_list().await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_server_create(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
req: CreateServerRequest,
|
||||
) -> ApiResult<ServerResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.server_create(&req).await
|
||||
}
|
||||
|
||||
// --- CATEGORY ---
|
||||
|
||||
#[command]
|
||||
pub async fn api_category_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
server_id: Option<String>,
|
||||
) -> ApiResult<Vec<CategoryResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.category_list(server_id.as_deref()).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_category_create(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
req: CreateCategoryRequest,
|
||||
) -> ApiResult<CategoryResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.category_create(&req).await
|
||||
}
|
||||
|
||||
// --- CHANNEL ---
|
||||
|
||||
#[command]
|
||||
pub async fn api_channel_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<Vec<ChannelResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.channel_list().await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_channel_create(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
req: CreateChannelRequest,
|
||||
) -> ApiResult<ChannelResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.channel_create(&req).await
|
||||
}
|
||||
|
||||
// --- MESSAGE ---
|
||||
|
||||
#[command]
|
||||
pub async fn api_message_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<Vec<MessageResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.message_list().await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn api_message_create(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
req: CreateMessageRequest,
|
||||
) -> ApiResult<MessageResponse> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.message_create(&req).await
|
||||
}
|
||||
|
||||
// --- USER ---
|
||||
|
||||
#[command]
|
||||
pub async fn api_user_list(
|
||||
state: State<'_, AppState>,
|
||||
base_url: String,
|
||||
) -> ApiResult<Vec<UserResponse>> {
|
||||
let client = state.api.get_or_create_client(base_url);
|
||||
client.user_list().await
|
||||
}
|
||||
7
src-tauri/src/api/mod.rs
Normal file
7
src-tauri/src/api/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod models;
|
||||
pub mod state;
|
||||
|
||||
pub use client::{ApiClient, ApiError, ApiResult};
|
||||
pub use state::ApiManager;
|
||||
115
src-tauri/src/api/models.rs
Normal file
115
src-tauri/src/api/models.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// --- AUTH ---
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct LoginRequest {
|
||||
pub username: String,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct LoginResponse {
|
||||
pub token: String,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ClaimAdminRequest {
|
||||
pub token: String,
|
||||
pub username: String,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SshChallengeResponse {
|
||||
pub challenge: String,
|
||||
}
|
||||
|
||||
// --- SERVER ---
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CreateServerRequest {
|
||||
pub name: String,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct ServerResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
// --- CATEGORY ---
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CreateCategoryRequest {
|
||||
pub server_id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CategoryResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
// --- CHANNEL ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChannelType {
|
||||
Text,
|
||||
Voice,
|
||||
DM,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CreateChannelRequest {
|
||||
pub channel_type: ChannelType,
|
||||
pub category_id: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub position: Option<i32>,
|
||||
pub server_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ChannelResponse {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub position: i32,
|
||||
pub channel_type: ChannelType,
|
||||
pub category_id: Option<String>,
|
||||
}
|
||||
|
||||
// --- MESSAGE ---
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CreateMessageRequest {
|
||||
pub channel_id: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct MessageResponse {
|
||||
pub id: String,
|
||||
pub channel_id: String,
|
||||
pub author_id: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
// --- USER ---
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct CreateUserRequest {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct UserResponse {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub pub_key: String,
|
||||
}
|
||||
35
src-tauri/src/api/state.rs
Normal file
35
src-tauri/src/api/state.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod ox_speak_app;
|
||||
pub mod ox_speak_app;
|
||||
pub mod state;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::config::ConfigManager;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub struct OxSpeakApp {
|
||||
tauri_handle: AppHandle,
|
||||
@@ -7,17 +7,7 @@ pub struct OxSpeakApp {
|
||||
}
|
||||
|
||||
impl OxSpeakApp {
|
||||
pub async fn new(tauri_handle: AppHandle) -> Self {
|
||||
// Chargement de la configuration
|
||||
let config_dir = tauri_handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.expect("Failed to get app config dir");
|
||||
|
||||
let config = ConfigManager::load(config_dir)
|
||||
.await
|
||||
.expect("Failed to load config");
|
||||
|
||||
pub async fn new(tauri_handle: AppHandle, config: ConfigManager) -> Self {
|
||||
Self {
|
||||
tauri_handle,
|
||||
config,
|
||||
|
||||
19
src-tauri/src/app/state.rs
Normal file
19
src-tauri/src/app/state.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::api::state::ApiManager;
|
||||
use crate::config::ConfigManager;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// État global de l'application.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AppState {
|
||||
pub api: ApiManager,
|
||||
pub config: OnceLock<ConfigManager>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
api: ApiManager::new(),
|
||||
config: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src-tauri/src/config/commands.rs
Normal file
21
src-tauri/src/config/commands.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use crate::app::state::AppState;
|
||||
use crate::config::config::ConfigTree;
|
||||
use tauri::{command, State};
|
||||
|
||||
#[command]
|
||||
pub async fn config_get(state: State<'_, AppState>) -> Result<ConfigTree, String> {
|
||||
let config = state.config.get().ok_or("Config non initialisée")?;
|
||||
Ok(config.get_config())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn config_update(
|
||||
state: State<'_, AppState>,
|
||||
new_config: ConfigTree,
|
||||
) -> Result<(), String> {
|
||||
let config = state.config.get().ok_or("Config non initialisée")?;
|
||||
config
|
||||
.update_config(new_config)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -5,35 +5,37 @@ use ssh_key::PrivateKey;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConfigManager {
|
||||
tree_config: Arc<Mutex<ConfigTree>>, // On garanti l'unicité avec un Mutex
|
||||
path: PathBuf,
|
||||
app_handle: Option<AppHandle>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
struct ConfigTree {
|
||||
pub struct ConfigTree {
|
||||
pub servers: Vec<ConfigServer>,
|
||||
pub identities: Vec<IdentityConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
struct ConfigServer {
|
||||
pub struct ConfigServer {
|
||||
pub adresse: String,
|
||||
pub identity: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum IdentityMode {
|
||||
pub enum IdentityMode {
|
||||
PrivateKeyPath,
|
||||
PrivateKeyBase64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
struct IdentityConfig {
|
||||
pub struct IdentityConfig {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub private_key: String,
|
||||
@@ -41,7 +43,26 @@ struct IdentityConfig {
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
pub async fn load(config_dir: PathBuf) -> Result<Self, std::io::Error> {
|
||||
pub fn get_config(&self) -> ConfigTree {
|
||||
self.tree_config.lock().clone()
|
||||
}
|
||||
|
||||
pub async fn update_config(&self, new_config: ConfigTree) -> Result<(), std::io::Error> {
|
||||
{
|
||||
let mut lock = self.tree_config.lock();
|
||||
*lock = new_config.clone();
|
||||
}
|
||||
self.save().await?;
|
||||
|
||||
// Notifier le front-end du changement
|
||||
if let Some(handle) = &self.app_handle {
|
||||
let _ = handle.emit("config-changed", new_config);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load(config_dir: PathBuf, app_handle: AppHandle) -> Result<Self, std::io::Error> {
|
||||
let config_path = config_dir.join("config.toml");
|
||||
|
||||
if config_path.exists() && config_path.is_file() {
|
||||
@@ -50,14 +71,15 @@ impl ConfigManager {
|
||||
return Ok(Self {
|
||||
tree_config: Arc::new(Mutex::new(tree_config)),
|
||||
path: config_path,
|
||||
app_handle: Some(app_handle),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Self::create(config_dir).await
|
||||
Self::create(config_dir, app_handle).await
|
||||
}
|
||||
|
||||
async fn create(config_dir: PathBuf) -> 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 tree_config = ConfigTree {
|
||||
@@ -68,6 +90,7 @@ impl ConfigManager {
|
||||
let config = ConfigManager {
|
||||
tree_config: Arc::new(Mutex::new(tree_config)),
|
||||
path: config_path,
|
||||
app_handle: Some(app_handle),
|
||||
};
|
||||
|
||||
config.save().await?;
|
||||
@@ -83,10 +106,11 @@ impl ConfigManager {
|
||||
}
|
||||
|
||||
// Lock le mutex pour lire tree_config
|
||||
let tree_config = self.tree_config.lock();
|
||||
let serialized = toml::to_string_pretty(&*tree_config)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
|
||||
drop(tree_config); // Libère le lock explicitement
|
||||
let serialized = {
|
||||
let tree_config = self.tree_config.lock();
|
||||
toml::to_string_pretty(&*tree_config)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
|
||||
};
|
||||
|
||||
fs::write(&self.path, serialized).await?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
mod config;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub use config::ConfigManager;
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
// .expect("error while running tauri application");
|
||||
// }
|
||||
|
||||
mod app;
|
||||
pub mod tauri_app;
|
||||
mod utils;
|
||||
mod app;
|
||||
|
||||
pub mod api;
|
||||
mod config;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::api::commands::*;
|
||||
use crate::app::ox_speak_app::OxSpeakApp;
|
||||
use crate::app::state::AppState;
|
||||
use crate::config::commands::*;
|
||||
use crate::config::ConfigManager;
|
||||
use std::sync::Arc;
|
||||
use tauri::{Manager, WindowEvent};
|
||||
|
||||
@@ -15,12 +19,43 @@ pub async fn run() {
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.manage(AppState::new())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
api_login,
|
||||
api_claim_admin,
|
||||
api_ssh_challenge,
|
||||
api_server_list,
|
||||
api_server_create,
|
||||
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,
|
||||
])
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
||||
// Démarrer le backend
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let ox_speak = Arc::new(OxSpeakApp::new(handle.clone()).await);
|
||||
let app_state = handle.state::<AppState>();
|
||||
let config_dir = handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.expect("Failed to get app config dir");
|
||||
|
||||
let config = ConfigManager::load(config_dir, handle.clone())
|
||||
.await
|
||||
.expect("Failed to load config");
|
||||
|
||||
// Mettre la config dans AppState
|
||||
let _ = app_state.config.set(config.clone());
|
||||
|
||||
let ox_speak = Arc::new(OxSpeakApp::new(handle.clone(), config).await);
|
||||
handle.manage(ox_speak.clone());
|
||||
ox_speak.run().await;
|
||||
});
|
||||
|
||||
68
src/api/README.md
Normal file
68
src/api/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# OxSpeak API Client (TypeScript)
|
||||
|
||||
Cette interface permet de communiquer avec les serveurs OxSpeak via Tauri (et plus tard via le Web).
|
||||
|
||||
## Installation
|
||||
|
||||
Assurez-vous d'avoir installé les dépendances Tauri dans votre projet frontend :
|
||||
|
||||
```bash
|
||||
yarn add @tauri-apps/api
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
L'API est conçue pour supporter plusieurs serveurs simultanément. Chaque serveur est représenté par une instance de
|
||||
`IApiClient`.
|
||||
|
||||
### Créer un client
|
||||
|
||||
```typescript
|
||||
import { createApiClient } from './api';
|
||||
|
||||
const client = createApiClient('http://localhost:8080');
|
||||
```
|
||||
|
||||
### Authentification
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const response = await client.login({
|
||||
username: 'mon_pseudo',
|
||||
password: 'mon_password'
|
||||
});
|
||||
console.log('Connecté ! Token:', response.token);
|
||||
} catch (error) {
|
||||
console.error('Erreur de connexion:', error);
|
||||
}
|
||||
```
|
||||
|
||||
### Lister les serveurs
|
||||
|
||||
```typescript
|
||||
const servers = await client.getServerList();
|
||||
servers.forEach(s => console.log(`Serveur: ${s.name} (${s.id})`));
|
||||
```
|
||||
|
||||
### Créer un canal
|
||||
|
||||
```typescript
|
||||
import { ChannelType } from './api';
|
||||
|
||||
const newChannel = await client.createChannel({
|
||||
name: 'Général',
|
||||
channel_type: 'text',
|
||||
server_id: 'server-123'
|
||||
});
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
L'interface est séparée en trois parties pour permettre une évolution vers une version Web sans casser le code
|
||||
existant :
|
||||
|
||||
1. `types.ts` : Les définitions d'interfaces de données (identiques aux modèles Rust).
|
||||
2. `client.ts` : L'interface `IApiClient` définissant le contrat.
|
||||
3. `tauri-client.ts` : L'implémentation concrète utilisant Tauri `invoke`.
|
||||
|
||||
Le fichier `index.ts` expose une usine `createApiClient` qui instancie la bonne implémentation.
|
||||
53
src/api/client.ts
Normal file
53
src/api/client.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type {
|
||||
CategoryResponse,
|
||||
ChannelResponse,
|
||||
ClaimAdminRequest,
|
||||
CreateCategoryRequest,
|
||||
CreateChannelRequest,
|
||||
CreateMessageRequest,
|
||||
CreateServerRequest,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
MessageResponse,
|
||||
ServerResponse,
|
||||
SshChallengeResponse,
|
||||
UserResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Interface générique pour l'API OxSpeak.
|
||||
* Elle permet de découpler l'implémentation (Tauri, Web, etc.) de l'utilisation.
|
||||
*/
|
||||
export interface IApiClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
// AUTH
|
||||
login(req: LoginRequest): Promise<LoginResponse>;
|
||||
|
||||
claimAdmin(req: ClaimAdminRequest): Promise<void>;
|
||||
|
||||
sshChallenge(): Promise<SshChallengeResponse>;
|
||||
|
||||
// SERVER
|
||||
getServerList(): Promise<ServerResponse[]>;
|
||||
|
||||
createServer(req: CreateServerRequest): Promise<ServerResponse>;
|
||||
|
||||
// CATEGORY
|
||||
getCategoryList(serverId?: string): Promise<CategoryResponse[]>;
|
||||
|
||||
createCategory(req: CreateCategoryRequest): Promise<CategoryResponse>;
|
||||
|
||||
// CHANNEL
|
||||
getChannelList(): Promise<ChannelResponse[]>;
|
||||
|
||||
createChannel(req: CreateChannelRequest): Promise<ChannelResponse>;
|
||||
|
||||
// MESSAGE
|
||||
getMessageList(): Promise<MessageResponse[]>;
|
||||
|
||||
createMessage(req: CreateMessageRequest): Promise<MessageResponse>;
|
||||
|
||||
// USER
|
||||
getUserList(): Promise<UserResponse[]>;
|
||||
}
|
||||
16
src/api/index.ts
Normal file
16
src/api/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export * from './types';
|
||||
export * from './client';
|
||||
export * from './tauri-client';
|
||||
|
||||
import {TauriApiClient} from './tauri-client';
|
||||
import type {IApiClient} from './client';
|
||||
|
||||
/**
|
||||
* Usine pour créer des clients API.
|
||||
* Plus tard, cette usine pourra retourner une implémentation Web ou Tauri
|
||||
* selon l'environnement (isTauri).
|
||||
*/
|
||||
export function createApiClient(baseUrl: string): IApiClient {
|
||||
// Pour le moment, on retourne systématiquement l'implémentation Tauri.
|
||||
return new TauriApiClient(baseUrl);
|
||||
}
|
||||
83
src/api/tauri-client.ts
Normal file
83
src/api/tauri-client.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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<LoginResponse> {
|
||||
return await invoke<LoginResponse>('api_login', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
async claimAdmin(req: ClaimAdminRequest): Promise<void> {
|
||||
return await invoke<void>('api_claim_admin', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
async sshChallenge(): Promise<SshChallengeResponse> {
|
||||
return await invoke<SshChallengeResponse>('api_ssh_challenge', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
// SERVER
|
||||
async getServerList(): Promise<ServerResponse[]> {
|
||||
return await invoke<ServerResponse[]>('api_server_list', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
async createServer(req: CreateServerRequest): Promise<ServerResponse> {
|
||||
return await invoke<ServerResponse>('api_server_create', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
// CATEGORY
|
||||
async getCategoryList(serverId?: string): Promise<CategoryResponse[]> {
|
||||
return await invoke<CategoryResponse[]>('api_category_list', {
|
||||
baseUrl: this.baseUrl,
|
||||
serverId: serverId ?? null
|
||||
});
|
||||
}
|
||||
|
||||
async createCategory(req: CreateCategoryRequest): Promise<CategoryResponse> {
|
||||
return await invoke<CategoryResponse>('api_category_create', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
// CHANNEL
|
||||
async getChannelList(): Promise<ChannelResponse[]> {
|
||||
return await invoke<ChannelResponse[]>('api_channel_list', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
async createChannel(req: CreateChannelRequest): Promise<ChannelResponse> {
|
||||
return await invoke<ChannelResponse>('api_channel_create', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
// MESSAGE
|
||||
async getMessageList(): Promise<MessageResponse[]> {
|
||||
return await invoke<MessageResponse[]>('api_message_list', {baseUrl: this.baseUrl});
|
||||
}
|
||||
|
||||
async createMessage(req: CreateMessageRequest): Promise<MessageResponse> {
|
||||
return await invoke<MessageResponse>('api_message_create', {baseUrl: this.baseUrl, req});
|
||||
}
|
||||
|
||||
// USER
|
||||
async getUserList(): Promise<UserResponse[]> {
|
||||
return await invoke<UserResponse[]>('api_user_list', {baseUrl: this.baseUrl});
|
||||
}
|
||||
}
|
||||
100
src/api/types.ts
Normal file
100
src/api/types.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// --- AUTH ---
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface ClaimAdminRequest {
|
||||
token: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface SshChallengeResponse {
|
||||
challenge: string;
|
||||
}
|
||||
|
||||
// --- SERVER ---
|
||||
|
||||
export interface CreateServerRequest {
|
||||
name: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface ServerResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// --- CATEGORY ---
|
||||
|
||||
export interface CreateCategoryRequest {
|
||||
server_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CategoryResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// --- CHANNEL ---
|
||||
|
||||
export type ChannelType = 'text' | 'voice' | 'd_m';
|
||||
|
||||
export interface CreateChannelRequest {
|
||||
channel_type: ChannelType;
|
||||
category_id?: string;
|
||||
name?: string;
|
||||
position?: number;
|
||||
server_id?: string;
|
||||
}
|
||||
|
||||
export interface ChannelResponse {
|
||||
id: string;
|
||||
name?: string;
|
||||
position: number;
|
||||
channel_type: ChannelType;
|
||||
category_id?: string;
|
||||
}
|
||||
|
||||
// --- MESSAGE ---
|
||||
|
||||
export interface CreateMessageRequest {
|
||||
channel_id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface MessageResponse {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
author_id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// --- USER ---
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
id: string;
|
||||
username: string;
|
||||
pub_key: string;
|
||||
}
|
||||
|
||||
// --- ERRORS ---
|
||||
|
||||
export interface ApiError {
|
||||
type: string;
|
||||
data: string | { status: number, body: string };
|
||||
}
|
||||
70
src/config/README.md
Normal file
70
src/config/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Système de Configuration OxSpeak
|
||||
|
||||
Ce module permet de gérer la configuration globale de l'application (serveurs enregistrés et identités).
|
||||
|
||||
## Utilisation
|
||||
|
||||
Le système utilise une interface générique `IConfigClient` qui permet d'utiliser le même code pour Tauri et
|
||||
une version Web (localStorage).
|
||||
|
||||
### Détection de l'environnement
|
||||
|
||||
La factory `createConfigClient()` détecte automatiquement si l'application tourne dans Tauri ou dans un navigateur
|
||||
standard :
|
||||
|
||||
- **Tauri** : Utilise les commandes Rust et le stockage TOML local.
|
||||
- **Web** : Utilise le `localStorage` du navigateur.
|
||||
|
||||
### Écoute des changements
|
||||
|
||||
Vous pouvez écouter les modifications de configuration de manière réactive :
|
||||
|
||||
```typescript
|
||||
const configClient = createConfigClient();
|
||||
|
||||
// S'abonner aux changements
|
||||
const unlisten = await configClient.onChanged((newConfig) => {
|
||||
console.log('La config a changé !', newConfig);
|
||||
});
|
||||
|
||||
// Plus tard, pour arrêter d'écouter
|
||||
// unlisten();
|
||||
```
|
||||
|
||||
### Exemple en TypeScript
|
||||
|
||||
```typescript
|
||||
import {createConfigClient} from '@/config';
|
||||
|
||||
const configClient = createConfigClient();
|
||||
|
||||
// Récupérer la configuration
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const config = await configClient.get();
|
||||
console.log('Serveurs configurés:', config.servers);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement de la config:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour la configuration
|
||||
async function addServer(name: string, identityId: string) {
|
||||
const config = await configClient.get();
|
||||
|
||||
config.servers.push({
|
||||
adresse: name,
|
||||
identity: identityId
|
||||
});
|
||||
|
||||
await configClient.update(config);
|
||||
console.log('Serveur ajouté !');
|
||||
}
|
||||
```
|
||||
|
||||
### Structures de données
|
||||
|
||||
- **ConfigTree** : La racine de la configuration contenant les serveurs et les identités.
|
||||
- **ConfigServer** : Définit un serveur (adresse et identité liée).
|
||||
- **IdentityConfig** : Définit une identité utilisateur avec sa clé privée SSH.
|
||||
- **IdentityMode** : `private_key_path` (chemin vers le fichier) ou `private_key_base64` (clé brute encodée).
|
||||
80
src/config/client.ts
Normal file
80
src/config/client.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {invoke} from '@tauri-apps/api/core';
|
||||
import {listen} from '@tauri-apps/api/event';
|
||||
import type {ConfigTree} from './types';
|
||||
|
||||
export interface IConfigClient {
|
||||
get(): Promise<ConfigTree>;
|
||||
|
||||
update(config: ConfigTree): Promise<void>;
|
||||
|
||||
/**
|
||||
* Écoute les changements de configuration.
|
||||
* Retourne une fonction pour arrêter l'écoute.
|
||||
*/
|
||||
onChanged(callback: (config: ConfigTree) => void): Promise<() => void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implémentation Tauri pour le client de configuration.
|
||||
*/
|
||||
export class TauriConfigClient implements IConfigClient {
|
||||
async get(): Promise<ConfigTree> {
|
||||
return await invoke<ConfigTree>('config_get');
|
||||
}
|
||||
|
||||
async update(config: ConfigTree): Promise<void> {
|
||||
await invoke('config_update', {newConfig: config});
|
||||
}
|
||||
|
||||
async onChanged(callback: (config: ConfigTree) => void): Promise<() => void> {
|
||||
const unlisten = await listen<ConfigTree>('config-changed', (event) => {
|
||||
callback(event.payload);
|
||||
});
|
||||
return unlisten;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implémentation Web (LocalStorage) pour le client de configuration.
|
||||
* Utile pour le développement hors-Tauri ou la future version Web.
|
||||
*/
|
||||
export class WebConfigClient implements IConfigClient {
|
||||
private STORAGE_KEY = 'ox-speak-config';
|
||||
|
||||
async get(): Promise<ConfigTree> {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (data) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse config from localStorage', e);
|
||||
}
|
||||
}
|
||||
return {servers: [], identities: []};
|
||||
}
|
||||
|
||||
async update(config: ConfigTree): Promise<void> {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(config));
|
||||
// Simuler un événement de changement pour les autres composants de la même page
|
||||
window.dispatchEvent(new CustomEvent('ox-config-changed', {detail: config}));
|
||||
}
|
||||
|
||||
async onChanged(callback: (config: ConfigTree) => void): Promise<() => void> {
|
||||
const handler = (event: any) => {
|
||||
callback(event.detail);
|
||||
};
|
||||
window.addEventListener('ox-config-changed', handler);
|
||||
return () => window.removeEventListener('ox-config-changed', handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Usine pour créer le client de configuration approprié.
|
||||
*/
|
||||
export function createConfigClient(): IConfigClient {
|
||||
// Détection automatique de l'environnement
|
||||
if ((window as any).__TAURI_INTERNALS__) {
|
||||
return new TauriConfigClient();
|
||||
}
|
||||
return new WebConfigClient();
|
||||
}
|
||||
2
src/config/index.ts
Normal file
2
src/config/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './client';
|
||||
18
src/config/types.ts
Normal file
18
src/config/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type IdentityMode = 'private_key_path' | 'private_key_base64';
|
||||
|
||||
export interface IdentityConfig {
|
||||
id: string;
|
||||
username: string;
|
||||
private_key: string;
|
||||
mode: IdentityMode;
|
||||
}
|
||||
|
||||
export interface ConfigServer {
|
||||
adresse: string;
|
||||
identity: string;
|
||||
}
|
||||
|
||||
export interface ConfigTree {
|
||||
servers: ConfigServer[];
|
||||
identities: IdentityConfig[];
|
||||
}
|
||||
Reference in New Issue
Block a user