end to end job management support
This commit is contained in:
		@@ -13,6 +13,7 @@ pub struct ServerBuilder {
 | 
			
		||||
    enable_auth: bool,
 | 
			
		||||
    enable_webhooks: bool,
 | 
			
		||||
    circle_worker_id: String,
 | 
			
		||||
    circles: HashMap<String, Vec<String>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ServerBuilder {
 | 
			
		||||
@@ -28,6 +29,7 @@ impl ServerBuilder {
 | 
			
		||||
            enable_auth: false,
 | 
			
		||||
            enable_webhooks: false,
 | 
			
		||||
            circle_worker_id: "default".to_string(),
 | 
			
		||||
            circles: HashMap::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@@ -72,6 +74,11 @@ impl ServerBuilder {
 | 
			
		||||
        self.enable_webhooks = true;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub fn circles(mut self, circles: HashMap<String, Vec<String>>) -> Self {
 | 
			
		||||
        self.circles = circles;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn build(self) -> Result<Server, TlsConfigError> {
 | 
			
		||||
        Ok(Server {
 | 
			
		||||
@@ -87,8 +94,10 @@ impl ServerBuilder {
 | 
			
		||||
            circle_worker_id: self.circle_worker_id,
 | 
			
		||||
            circle_name: "default".to_string(),
 | 
			
		||||
            circle_public_key: "default".to_string(),
 | 
			
		||||
            circles: self.circles,
 | 
			
		||||
            nonce_store: HashMap::new(),
 | 
			
		||||
            authenticated_pubkey: None,
 | 
			
		||||
            dispatcher: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										220
									
								
								interfaces/websocket/server/src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								interfaces/websocket/server/src/config.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,220 @@
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct ServerConfig {
 | 
			
		||||
    /// Server host address
 | 
			
		||||
    #[serde(default = "default_host")]
 | 
			
		||||
    pub host: String,
 | 
			
		||||
    
 | 
			
		||||
    /// Server port
 | 
			
		||||
    #[serde(default = "default_port")]
 | 
			
		||||
    pub port: u16,
 | 
			
		||||
    
 | 
			
		||||
    /// Redis connection URL
 | 
			
		||||
    #[serde(default = "default_redis_url")]
 | 
			
		||||
    pub redis_url: String,
 | 
			
		||||
    
 | 
			
		||||
    /// Enable authentication
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    pub auth: bool,
 | 
			
		||||
    
 | 
			
		||||
    /// Enable TLS/WSS
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    pub tls: bool,
 | 
			
		||||
    
 | 
			
		||||
    /// Path to TLS certificate file
 | 
			
		||||
    pub cert: Option<String>,
 | 
			
		||||
    
 | 
			
		||||
    /// Path to TLS private key file
 | 
			
		||||
    pub key: Option<String>,
 | 
			
		||||
    
 | 
			
		||||
    /// Separate port for TLS connections
 | 
			
		||||
    pub tls_port: Option<u16>,
 | 
			
		||||
    
 | 
			
		||||
    /// Enable webhook handling
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    pub webhooks: bool,
 | 
			
		||||
    
 | 
			
		||||
    /// Circles configuration - maps circle names to lists of member public keys
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    pub circles: HashMap<String, Vec<String>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for ServerConfig {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            host: default_host(),
 | 
			
		||||
            port: default_port(),
 | 
			
		||||
            redis_url: default_redis_url(),
 | 
			
		||||
            auth: false,
 | 
			
		||||
            tls: false,
 | 
			
		||||
            cert: None,
 | 
			
		||||
            key: None,
 | 
			
		||||
            tls_port: None,
 | 
			
		||||
            webhooks: false,
 | 
			
		||||
            circles: HashMap::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ServerConfig {
 | 
			
		||||
    /// Load configuration from a JSON file
 | 
			
		||||
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
 | 
			
		||||
        let content = fs::read_to_string(path.as_ref())
 | 
			
		||||
            .map_err(|e| ConfigError::FileRead(path.as_ref().to_path_buf(), e))?;
 | 
			
		||||
        
 | 
			
		||||
        let config: ServerConfig = serde_json::from_str(&content)
 | 
			
		||||
            .map_err(|e| ConfigError::JsonParse(e))?;
 | 
			
		||||
        
 | 
			
		||||
        config.validate()?;
 | 
			
		||||
        Ok(config)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Save configuration to a JSON file
 | 
			
		||||
    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
 | 
			
		||||
        let content = serde_json::to_string_pretty(self)
 | 
			
		||||
            .map_err(|e| ConfigError::JsonSerialize(e))?;
 | 
			
		||||
        
 | 
			
		||||
        fs::write(path.as_ref(), content)
 | 
			
		||||
            .map_err(|e| ConfigError::FileWrite(path.as_ref().to_path_buf(), e))?;
 | 
			
		||||
        
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Validate the configuration
 | 
			
		||||
    pub fn validate(&self) -> Result<(), ConfigError> {
 | 
			
		||||
        // Validate TLS configuration
 | 
			
		||||
        if self.tls && (self.cert.is_none() || self.key.is_none()) {
 | 
			
		||||
            return Err(ConfigError::InvalidTlsConfig(
 | 
			
		||||
                "TLS is enabled but certificate or key path is missing".to_string()
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Validate that circles are not empty if auth is enabled
 | 
			
		||||
        if self.auth && self.circles.is_empty() {
 | 
			
		||||
            return Err(ConfigError::InvalidAuthConfig(
 | 
			
		||||
                "Authentication is enabled but no circles are configured".to_string()
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Create a sample configuration file
 | 
			
		||||
    pub fn create_sample() -> Self {
 | 
			
		||||
        let mut circles = HashMap::new();
 | 
			
		||||
        circles.insert(
 | 
			
		||||
            "example_circle".to_string(),
 | 
			
		||||
            vec![
 | 
			
		||||
                "0x1234567890abcdef1234567890abcdef12345678".to_string(),
 | 
			
		||||
                "0xabcdef1234567890abcdef1234567890abcdef12".to_string(),
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
        
 | 
			
		||||
        Self {
 | 
			
		||||
            host: "127.0.0.1".to_string(),
 | 
			
		||||
            port: 8443,
 | 
			
		||||
            redis_url: "redis://127.0.0.1/".to_string(),
 | 
			
		||||
            auth: true,
 | 
			
		||||
            tls: false,
 | 
			
		||||
            cert: Some("cert.pem".to_string()),
 | 
			
		||||
            key: Some("key.pem".to_string()),
 | 
			
		||||
            tls_port: Some(8444),
 | 
			
		||||
            webhooks: false,
 | 
			
		||||
            circles,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, thiserror::Error)]
 | 
			
		||||
pub enum ConfigError {
 | 
			
		||||
    #[error("Failed to read config file {0}: {1}")]
 | 
			
		||||
    FileRead(std::path::PathBuf, std::io::Error),
 | 
			
		||||
    
 | 
			
		||||
    #[error("Failed to write config file {0}: {1}")]
 | 
			
		||||
    FileWrite(std::path::PathBuf, std::io::Error),
 | 
			
		||||
    
 | 
			
		||||
    #[error("Failed to parse JSON config: {0}")]
 | 
			
		||||
    JsonParse(serde_json::Error),
 | 
			
		||||
    
 | 
			
		||||
    #[error("Failed to serialize JSON config: {0}")]
 | 
			
		||||
    JsonSerialize(serde_json::Error),
 | 
			
		||||
    
 | 
			
		||||
    #[error("Invalid TLS configuration: {0}")]
 | 
			
		||||
    InvalidTlsConfig(String),
 | 
			
		||||
    
 | 
			
		||||
    #[error("Invalid authentication configuration: {0}")]
 | 
			
		||||
    InvalidAuthConfig(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Default value functions
 | 
			
		||||
fn default_host() -> String {
 | 
			
		||||
    "127.0.0.1".to_string()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn default_port() -> u16 {
 | 
			
		||||
    8443
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn default_redis_url() -> String {
 | 
			
		||||
    "redis://127.0.0.1/".to_string()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
    use tempfile::NamedTempFile;
 | 
			
		||||
    
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_config_serialization() {
 | 
			
		||||
        let config = ServerConfig::create_sample();
 | 
			
		||||
        let json = serde_json::to_string_pretty(&config).unwrap();
 | 
			
		||||
        let deserialized: ServerConfig = serde_json::from_str(&json).unwrap();
 | 
			
		||||
        
 | 
			
		||||
        assert_eq!(config.host, deserialized.host);
 | 
			
		||||
        assert_eq!(config.port, deserialized.port);
 | 
			
		||||
        assert_eq!(config.circles.len(), deserialized.circles.len());
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_config_file_operations() {
 | 
			
		||||
        let config = ServerConfig::create_sample();
 | 
			
		||||
        let temp_file = NamedTempFile::new().unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Test writing
 | 
			
		||||
        config.to_file(temp_file.path()).unwrap();
 | 
			
		||||
        
 | 
			
		||||
        // Test reading
 | 
			
		||||
        let loaded_config = ServerConfig::from_file(temp_file.path()).unwrap();
 | 
			
		||||
        assert_eq!(config.host, loaded_config.host);
 | 
			
		||||
        assert_eq!(config.circles.len(), loaded_config.circles.len());
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_config_validation() {
 | 
			
		||||
        let mut config = ServerConfig::default();
 | 
			
		||||
        
 | 
			
		||||
        // Valid config should pass
 | 
			
		||||
        assert!(config.validate().is_ok());
 | 
			
		||||
        
 | 
			
		||||
        // TLS enabled without cert/key should fail
 | 
			
		||||
        config.tls = true;
 | 
			
		||||
        assert!(config.validate().is_err());
 | 
			
		||||
        
 | 
			
		||||
        // Fix TLS config
 | 
			
		||||
        config.cert = Some("cert.pem".to_string());
 | 
			
		||||
        config.key = Some("key.pem".to_string());
 | 
			
		||||
        assert!(config.validate().is_ok());
 | 
			
		||||
        
 | 
			
		||||
        // Auth enabled without circles should fail
 | 
			
		||||
        config.auth = true;
 | 
			
		||||
        assert!(config.validate().is_err());
 | 
			
		||||
        
 | 
			
		||||
        // Add circles
 | 
			
		||||
        config.circles.insert("test".to_string(), vec!["pubkey".to_string()]);
 | 
			
		||||
        assert!(config.validate().is_ok());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,6 +31,16 @@ impl actix::StreamHandler<Result<ws::Message, ws::ProtocolError>> for Server {
 | 
			
		||||
                                self.handle_whoami(req.params, client_rpc_id, ctx)
 | 
			
		||||
                            }
 | 
			
		||||
                            "play" => self.handle_play(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "create_job" => self.handle_create_job(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "start_job" => self.handle_start_job(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "run_job" => self.handle_run_job(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "get_job_status" => self.handle_get_job_status(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "get_job_output" => self.handle_get_job_output(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "get_job_logs" => self.handle_get_job_logs(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "list_jobs" => self.handle_list_jobs(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "stop_job" => self.handle_stop_job(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "delete_job" => self.handle_delete_job(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            "clear_all_jobs" => self.handle_clear_all_jobs(req.params, client_rpc_id, ctx),
 | 
			
		||||
                            _ => {
 | 
			
		||||
                                let err_resp = JsonRpcResponse {
 | 
			
		||||
                                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										999
									
								
								interfaces/websocket/server/src/job_handlers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										999
									
								
								interfaces/websocket/server/src/job_handlers.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,999 @@
 | 
			
		||||
use crate::Server;
 | 
			
		||||
use actix::prelude::*;
 | 
			
		||||
use actix_web_actors::ws;
 | 
			
		||||
use hero_dispatcher::{Dispatcher, ScriptType};
 | 
			
		||||
use serde_json::{json, Value};
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
 | 
			
		||||
const TASK_TIMEOUT_DURATION: Duration = Duration::from_secs(30);
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct SuccessResult {
 | 
			
		||||
    success: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct JobResult {
 | 
			
		||||
    job_id: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct JsonRpcResponse {
 | 
			
		||||
    jsonrpc: String,
 | 
			
		||||
    result: Option<Value>,
 | 
			
		||||
    error: Option<JsonRpcError>,
 | 
			
		||||
    id: Value,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct JsonRpcError {
 | 
			
		||||
    code: i32,
 | 
			
		||||
    message: String,
 | 
			
		||||
    data: Option<Value>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Server {
 | 
			
		||||
    pub fn handle_create_job(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        // For now, create_job is the same as run_job
 | 
			
		||||
        self.handle_run_job(params, client_rpc_id, ctx);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_start_job(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let job_id = match params.get("job_id").and_then(|v| v.as_str()) {
 | 
			
		||||
            Some(id) => id.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: "Missing required parameter: job_id".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher.start_job(&job_id).await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(_) => {
 | 
			
		||||
                        let result = SuccessResult { success: true };
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(serde_json::to_value(result).unwrap()),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to start job: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_get_job_status(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let job_id = match params.get("job_id").and_then(|v| v.as_str()) {
 | 
			
		||||
            Some(id) => id.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: "Missing required parameter: job_id".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher.get_job_status(&job_id).await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(status) => {
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(json!(status)),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to get job status: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_list_jobs(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        _params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher.list_jobs().await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(jobs) => {
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(json!(jobs)),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to list jobs: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    pub fn handle_run_job(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let circle_pk = match params.get("circle_pk").and_then(|v| v.as_str()) {
 | 
			
		||||
            Some(pk) => pk.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: "Missing required parameter: circle_pk".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let script_content = match params.get("script_content").and_then(|v| v.as_str()) {
 | 
			
		||||
            Some(script) => script.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: "Missing required parameter: script_content".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher
 | 
			
		||||
                .new_job()
 | 
			
		||||
                .context_id(&circle_pk)
 | 
			
		||||
                .script_type(ScriptType::RhaiSAL)
 | 
			
		||||
                .script(&script_content)
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .await_response()
 | 
			
		||||
                .await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(job_id) => {
 | 
			
		||||
                        let result = JobResult { job_id };
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(serde_json::to_value(result).unwrap()),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to run job: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_get_job_output(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let job_id = match params.get("job_id").and_then(|v| v.as_str()) {
 | 
			
		||||
            Some(id) => id.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: "Missing required parameter: job_id".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher.get_job_output(&job_id).await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(output) => {
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(json!(output)),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to get job output: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_get_job_logs(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let job_id = match params.get("job_id").and_then(|v| v.as_str()) {
 | 
			
		||||
            Some(id) => id.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: "Missing required parameter: job_id".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher.get_job_logs(&job_id).await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(logs) => {
 | 
			
		||||
                        let result = json!({ "logs": logs });
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(result),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to get job logs: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_stop_job(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let job_id = match params.get("job_id").and_then(|v| v.as_str()) {
 | 
			
		||||
            Some(id) => id.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: "Missing required parameter: job_id".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher.stop_job(&job_id).await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(_) => {
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(json!(null)),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to stop job: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_delete_job(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let job_id = match params.get("job_id").and_then(|v| v.as_str()) {
 | 
			
		||||
            Some(id) => id.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: "Missing required parameter: job_id".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher.delete_job(&job_id).await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(_) => {
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(json!(null)),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to delete job: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn handle_clear_all_jobs(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        _params: Value,
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        if self.enable_auth && !self.is_connection_authenticated() {
 | 
			
		||||
            let err_resp = JsonRpcResponse {
 | 
			
		||||
                jsonrpc: "2.0".to_string(),
 | 
			
		||||
                result: None,
 | 
			
		||||
                error: Some(JsonRpcError {
 | 
			
		||||
                    code: -32000,
 | 
			
		||||
                    message: "Authentication required".to_string(),
 | 
			
		||||
                    data: None,
 | 
			
		||||
                }),
 | 
			
		||||
                id: client_rpc_id,
 | 
			
		||||
            };
 | 
			
		||||
            ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let dispatcher = match self.dispatcher.clone() {
 | 
			
		||||
            Some(d) => d,
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32603,
 | 
			
		||||
                        message: "Internal error: dispatcher not available".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let client_rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
        let fut = async move {
 | 
			
		||||
            dispatcher.clear_all_jobs().await
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ctx.spawn(
 | 
			
		||||
            fut.into_actor(self)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                    Ok(_) => {
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(json!(null)),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id_clone.clone(),
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: format!("Failed to clear jobs: {}", e),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id_clone,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                .map(move |res, _act, ctx_inner| {
 | 
			
		||||
                    if res.is_err() {
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32000,
 | 
			
		||||
                                message: "Request timed out".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,7 +3,8 @@ use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
 | 
			
		||||
use actix_web_actors::ws;
 | 
			
		||||
use log::{info, error}; // Added error for better logging
 | 
			
		||||
use once_cell::sync::Lazy;
 | 
			
		||||
use hero_dispatcher::{DispatcherBuilder, DispatcherError};
 | 
			
		||||
use hero_dispatcher::{Dispatcher, DispatcherBuilder, DispatcherError};
 | 
			
		||||
use hero_job::{Job, JobStatus};
 | 
			
		||||
use rustls::pki_types::PrivateKeyDer;
 | 
			
		||||
use rustls::ServerConfig as RustlsServerConfig;
 | 
			
		||||
use rustls_pemfile::{certs, pkcs8_private_keys};
 | 
			
		||||
@@ -29,10 +30,13 @@ static AUTHENTICATED_CONNECTIONS: Lazy<Mutex<HashMap<Addr<Server>, String>>> =
 | 
			
		||||
 | 
			
		||||
mod auth;
 | 
			
		||||
mod builder;
 | 
			
		||||
mod config;
 | 
			
		||||
mod handler;
 | 
			
		||||
mod job_handlers;
 | 
			
		||||
 | 
			
		||||
use crate::auth::{generate_nonce, NonceResponse};
 | 
			
		||||
pub use crate::builder::ServerBuilder;
 | 
			
		||||
pub use crate::config::{ServerConfig, ConfigError};
 | 
			
		||||
// Re-export server handle type for external use
 | 
			
		||||
pub type ServerHandle = actix_web::dev::ServerHandle;
 | 
			
		||||
 | 
			
		||||
@@ -100,6 +104,64 @@ struct FetchNonceParams {
 | 
			
		||||
    pubkey: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Job management parameter structures
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct CreateJobParams {
 | 
			
		||||
    job: Job,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct RunJobParams {
 | 
			
		||||
    job: Job,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct JobIdParams {
 | 
			
		||||
    job_id: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Job management result structures
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct CreateJobResult {
 | 
			
		||||
    job_id: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct RunJobResult {
 | 
			
		||||
    job_id: String,
 | 
			
		||||
    output: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct JobStatusResult {
 | 
			
		||||
    status: JobStatus,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct JobOutputResult {
 | 
			
		||||
    output: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct JobLogsResult {
 | 
			
		||||
    logs: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct ListJobsResult {
 | 
			
		||||
    jobs: Vec<Job>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct SuccessResult {
 | 
			
		||||
    success: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize)]
 | 
			
		||||
struct ClearJobsResult {
 | 
			
		||||
    deleted_count: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Actor for Server {
 | 
			
		||||
    type Context = ws::WebsocketContext<Self>;
 | 
			
		||||
 | 
			
		||||
@@ -142,11 +204,14 @@ pub struct Server {
 | 
			
		||||
    pub tls_port: Option<u16>,
 | 
			
		||||
    pub enable_auth: bool,
 | 
			
		||||
    pub enable_webhooks: bool,
 | 
			
		||||
    pub circle_worker_id: String,
 | 
			
		||||
 | 
			
		||||
    pub circle_name: String,
 | 
			
		||||
    pub circle_public_key: String,
 | 
			
		||||
    /// Map of circle IDs to vectors of public keys that are members of that circle
 | 
			
		||||
    pub circles: HashMap<String, Vec<String>>,
 | 
			
		||||
    nonce_store: HashMap<String, NonceResponse>,
 | 
			
		||||
    authenticated_pubkey: Option<String>,
 | 
			
		||||
    pub dispatcher: Option<Dispatcher>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Server {
 | 
			
		||||
@@ -250,33 +315,34 @@ impl Server {
 | 
			
		||||
        client_rpc_id: Value,
 | 
			
		||||
        ctx: &mut ws::WebsocketContext<Self>,
 | 
			
		||||
    ) {
 | 
			
		||||
        match serde_json::from_value::<FetchNonceParams>(params) {
 | 
			
		||||
            Ok(params) => {
 | 
			
		||||
                let nonce_response = generate_nonce();
 | 
			
		||||
                self.nonce_store
 | 
			
		||||
                    .insert(params.pubkey, nonce_response.clone());
 | 
			
		||||
                let resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: Some(serde_json::to_value(nonce_response).unwrap()),
 | 
			
		||||
                    error: None,
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
            }
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
        // Extract pubkey string directly from params
 | 
			
		||||
        let pubkey = match params.as_str() {
 | 
			
		||||
            Some(pk) => pk.to_string(),
 | 
			
		||||
            None => {
 | 
			
		||||
                let err_resp = JsonRpcResponse {
 | 
			
		||||
                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                    result: None,
 | 
			
		||||
                    error: Some(JsonRpcError {
 | 
			
		||||
                        code: -32602,
 | 
			
		||||
                        message: format!("Invalid parameters for fetch_nonce: {}", e),
 | 
			
		||||
                        message: "Invalid parameters for fetch_nonce: expected string pubkey".to_string(),
 | 
			
		||||
                        data: None,
 | 
			
		||||
                    }),
 | 
			
		||||
                    id: client_rpc_id,
 | 
			
		||||
                };
 | 
			
		||||
                ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let nonce_response = generate_nonce();
 | 
			
		||||
        self.nonce_store.insert(pubkey, nonce_response.clone());
 | 
			
		||||
        let resp = JsonRpcResponse {
 | 
			
		||||
            jsonrpc: "2.0".to_string(),
 | 
			
		||||
            result: Some(serde_json::to_value(&nonce_response.nonce).unwrap()),
 | 
			
		||||
            error: None,
 | 
			
		||||
            id: client_rpc_id,
 | 
			
		||||
        };
 | 
			
		||||
        ctx.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_authenticate(
 | 
			
		||||
@@ -327,18 +393,41 @@ impl Server {
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                if is_valid {
 | 
			
		||||
                    self.authenticated_pubkey = Some(auth_params.pubkey.clone());
 | 
			
		||||
                    AUTHENTICATED_CONNECTIONS
 | 
			
		||||
                        .lock()
 | 
			
		||||
                        .unwrap()
 | 
			
		||||
                        .insert(ctx.address(), auth_params.pubkey);
 | 
			
		||||
                    let resp = JsonRpcResponse {
 | 
			
		||||
                        jsonrpc: "2.0".to_string(),
 | 
			
		||||
                        result: Some(serde_json::json!({ "authenticated": true })),
 | 
			
		||||
                        error: None,
 | 
			
		||||
                        id: client_rpc_id,
 | 
			
		||||
                    };
 | 
			
		||||
                    ctx.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    // Check if the authenticated public key belongs to the circle
 | 
			
		||||
                    let is_circle_member = self.circles
 | 
			
		||||
                        .get(&self.circle_name)
 | 
			
		||||
                        .map(|members| members.contains(&auth_params.pubkey))
 | 
			
		||||
                        .unwrap_or(false);
 | 
			
		||||
                    
 | 
			
		||||
                    if is_circle_member {
 | 
			
		||||
                        self.authenticated_pubkey = Some(auth_params.pubkey.clone());
 | 
			
		||||
                        AUTHENTICATED_CONNECTIONS
 | 
			
		||||
                            .lock()
 | 
			
		||||
                            .unwrap()
 | 
			
		||||
                            .insert(ctx.address(), auth_params.pubkey);
 | 
			
		||||
                        let resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: Some(serde_json::json!({ "authenticated": true })),
 | 
			
		||||
                            error: None,
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                    } else {
 | 
			
		||||
                        log::warn!("Auth failed for {}: Public key {} not a member of circle {}", 
 | 
			
		||||
                                  self.circle_name, auth_params.pubkey, self.circle_name);
 | 
			
		||||
                        let err_resp = JsonRpcResponse {
 | 
			
		||||
                            jsonrpc: "2.0".to_string(),
 | 
			
		||||
                            result: None,
 | 
			
		||||
                            error: Some(JsonRpcError {
 | 
			
		||||
                                code: -32003,
 | 
			
		||||
                                message: "Public key not authorized for this circle".to_string(),
 | 
			
		||||
                                data: None,
 | 
			
		||||
                            }),
 | 
			
		||||
                            id: client_rpc_id,
 | 
			
		||||
                        };
 | 
			
		||||
                        ctx.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                        ctx.stop();
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    let err_resp = JsonRpcResponse {
 | 
			
		||||
                        jsonrpc: "2.0".to_string(),
 | 
			
		||||
@@ -459,7 +548,7 @@ impl Server {
 | 
			
		||||
                let redis_url_clone = self.redis_url.clone();
 | 
			
		||||
                let _rpc_id_clone = client_rpc_id.clone();
 | 
			
		||||
                let public_key = self.authenticated_pubkey.clone();
 | 
			
		||||
                let worker_id_clone = self.circle_worker_id.clone();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                let fut = async move {
 | 
			
		||||
                    let caller_id = public_key.unwrap_or_else(|| "anonymous".to_string());
 | 
			
		||||
@@ -471,7 +560,7 @@ impl Server {
 | 
			
		||||
                            hero_dispatcher
 | 
			
		||||
                                .new_job()
 | 
			
		||||
                                    .context_id(&circle_pk_clone)
 | 
			
		||||
                                    .worker_id(&worker_id_clone)
 | 
			
		||||
                                    .script_type(hero_dispatcher::ScriptType::RhaiSAL)
 | 
			
		||||
                                    .script(&script_content)
 | 
			
		||||
                                    .timeout(TASK_TIMEOUT_DURATION)
 | 
			
		||||
                                    .await_response()
 | 
			
		||||
@@ -484,35 +573,16 @@ impl Server {
 | 
			
		||||
                ctx.spawn(
 | 
			
		||||
                    fut.into_actor(self)
 | 
			
		||||
                        .map(move |res, _act, ctx_inner| match res {
 | 
			
		||||
                            Ok(task_details) => {
 | 
			
		||||
                                if task_details.status == "completed" {
 | 
			
		||||
                                    let output = task_details
 | 
			
		||||
                                        .output
 | 
			
		||||
                                        .unwrap_or_else(|| "No output".to_string());
 | 
			
		||||
                                    let result_value = PlayResult { output };
 | 
			
		||||
                                    let resp = JsonRpcResponse {
 | 
			
		||||
                                        jsonrpc: "2.0".to_string(),
 | 
			
		||||
                                        result: Some(serde_json::to_value(result_value).unwrap()),
 | 
			
		||||
                                        error: None,
 | 
			
		||||
                                        id: client_rpc_id,
 | 
			
		||||
                                    };
 | 
			
		||||
                                    ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    let error_message = task_details.error.unwrap_or_else(|| {
 | 
			
		||||
                                        "Rhai script execution failed".to_string()
 | 
			
		||||
                                    });
 | 
			
		||||
                                    let err_resp = JsonRpcResponse {
 | 
			
		||||
                                        jsonrpc: "2.0".to_string(),
 | 
			
		||||
                                        result: None,
 | 
			
		||||
                                        error: Some(JsonRpcError {
 | 
			
		||||
                                            code: -32000,
 | 
			
		||||
                                            message: error_message,
 | 
			
		||||
                                            data: None,
 | 
			
		||||
                                        }),
 | 
			
		||||
                                        id: client_rpc_id,
 | 
			
		||||
                                    };
 | 
			
		||||
                                    ctx_inner.text(serde_json::to_string(&err_resp).unwrap());
 | 
			
		||||
                                }
 | 
			
		||||
                            Ok(output) => {
 | 
			
		||||
                                // The dispatcher returns the actual string output from job execution
 | 
			
		||||
                                let result_value = PlayResult { output };
 | 
			
		||||
                                let resp = JsonRpcResponse {
 | 
			
		||||
                                    jsonrpc: "2.0".to_string(),
 | 
			
		||||
                                    result: Some(serde_json::to_value(result_value).unwrap()),
 | 
			
		||||
                                    error: None,
 | 
			
		||||
                                    id: client_rpc_id,
 | 
			
		||||
                                };
 | 
			
		||||
                                ctx_inner.text(serde_json::to_string(&resp).unwrap());
 | 
			
		||||
                            }
 | 
			
		||||
                            Err(e) => {
 | 
			
		||||
                                let (code, message) = match e {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user