chahinebrini 2919ce45b8 feat(magic): sync current ReBreak Magic app state
Include recent Magic app work: Tauri native shell, iOS device detection
via supervise-magic sidecar, MDM client, local HTTP server, new pages
(detect, enroll, supervise, sideload, pair, preflight, configure, done),
and updated device section/status UI.
2026-06-18 05:23:26 +02:00

143 lines
4.5 KiB
Rust

use crate::config::AppConfig;
use crate::error::{AppError, AppResult};
use serde::{Deserialize, Serialize};
const HTTP_TIMEOUT_SECONDS: u64 = 30;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MdmPushStatus {
pub udid: String,
pub push_result: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MdmEnqueueResult {
pub command_uuid: String,
pub response_body: String,
}
pub struct MdmClient {
client: reqwest::Client,
server: String,
auth_header: String,
}
impl MdmClient {
pub fn new() -> AppResult<Self> {
let cfg = AppConfig::load_binder_config()?;
let creds = format!("{}:{}", cfg.mdm_user, cfg.mdm_api_key);
use base64::{engine::general_purpose::STANDARD, Engine};
let auth_header = format!("Basic {}", STANDARD.encode(creds));
Ok(Self {
client: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECONDS))
.build()
.map_err(|e| AppError::new(format!("reqwest client build: {}", e)))?,
server: cfg.mdm_server,
auth_header,
})
}
fn url(&self, path: &str) -> AppResult<String> {
let base = self.server.trim_end_matches('/');
Ok(format!("{}{}", base, path))
}
fn request(&self, method: reqwest::Method, path: &str) -> AppResult<reqwest::RequestBuilder> {
let url = self.url(path)?;
Ok(self
.client
.request(method, &url)
.header("Authorization", &self.auth_header))
}
pub async fn ping(&self) -> AppResult<String> {
let resp = self
.request(reqwest::Method::GET, "/version")?
.send()
.await
.map_err(|e| AppError::new(format!("MDM ping failed: {}", e)))?;
let status = resp.status();
let body = resp
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
if !status.is_success() {
return Err(AppError::new(format!("MDM ping HTTP {}: {}", status, body)));
}
Ok(body)
}
pub async fn push(&self, udid: &str) -> AppResult<MdmPushStatus> {
let resp = self
.request(reqwest::Method::GET, &format!("/v1/push/{}", udid))?
.send()
.await
.map_err(|e| AppError::new(format!("MDM push failed: {}", e)))?;
let status = resp.status();
let body = resp
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
if !status.is_success() {
return Err(AppError::new(format!("MDM push HTTP {}: {}", status, body)));
}
// Parse NanoMDM response: { "status": { "<udid>": { "push_result": "..." } } }
let parsed: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| AppError::new(format!("MDM push parse error: {} — body: {}", e, body)))?;
let push_result = parsed
.get("status")
.and_then(|s| s.get(udid))
.and_then(|d| d.get("push_result"))
.and_then(|v| v.as_str())
.ok_or_else(|| AppError::new(format!("MDM push response unerwartet: {}", body)))?
.to_string();
Ok(MdmPushStatus {
udid: udid.to_string(),
push_result,
})
}
pub async fn enqueue(&self, udid: &str, command: serde_json::Value) -> AppResult<MdmEnqueueResult> {
let command_uuid = uuid::Uuid::new_v4().to_string();
let envelope = serde_json::json!({
"CommandUUID": command_uuid,
"Command": command,
});
// NanoMDM expects plist, but also accepts JSON in newer versions.
// For maximum compatibility we send JSON here; if it fails, the caller
// will see the error.
let resp = self
.request(reqwest::Method::PUT, &format!("/v1/enqueue/{}?push=1", udid))?
.header("Content-Type", "application/json")
.json(&envelope)
.send()
.await
.map_err(|e| AppError::new(format!("MDM enqueue failed: {}", e)))?;
let status = resp.status();
let body = resp
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
if !status.is_success() {
return Err(AppError::new(format!("MDM enqueue HTTP {}: {}", status, body)));
}
Ok(MdmEnqueueResult {
command_uuid,
response_body: body,
})
}
}