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

504 lines
16 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 RedeemPairingRequest {
pub code: String,
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedeemPairingResponse {
pub token: String,
#[serde(rename = "sessionId")]
pub session_id: String,
#[serde(rename = "createdAt")]
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterDeviceRequest {
#[serde(rename = "deviceId", skip_serializing_if = "Option::is_none")]
pub device_id: Option<String>,
#[serde(rename = "hardwareId", skip_serializing_if = "Option::is_none")]
pub hardware_id: Option<String>,
pub hostname: String,
pub model: Option<String>,
#[serde(rename = "osVersion")]
pub os_version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterDeviceResponse {
#[serde(rename = "deviceId")]
pub device_id: String,
#[serde(rename = "dnsToken")]
pub dns_token: String,
#[serde(rename = "profileUrl")]
pub profile_url: String,
pub existing: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MagicDeviceInfo {
pub source: String,
#[serde(rename = "deviceId")]
pub device_id: String,
#[serde(rename = "hardwareId")]
pub hardware_id: Option<String>,
pub hostname: String,
pub model: Option<String>,
#[serde(rename = "osVersion")]
pub os_version: Option<String>,
#[serde(default, rename = "mdmId")]
pub mdm_id: Option<String>,
#[serde(rename = "magicEnrolledAt")]
pub magic_enrolled_at: Option<String>,
#[serde(rename = "releaseRequestedAt")]
pub release_requested_at: Option<String>,
#[serde(rename = "releaseAvailableAt")]
pub release_available_at: Option<String>,
#[serde(default, rename = "cooldownUntil")]
pub cooldown_until: Option<String>,
#[serde(default = "default_active_status")]
pub status: String,
#[serde(default, rename = "lastSeenAt")]
pub last_seen_at: Option<String>,
}
fn default_active_status() -> String {
"active".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusResponse {
pub active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseResponse {
#[serde(rename = "releaseRequestedAt")]
pub release_requested_at: String,
#[serde(rename = "releaseAvailableAt")]
pub release_available_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CooldownResponse {
#[serde(rename = "cooldownUntil")]
pub cooldown_until: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserProfile {
pub nickname: Option<String>,
pub avatar: Option<String>,
pub plan: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MdmStatusData {
pub enrolled: bool,
pub company: Option<String>,
pub supervised: bool,
#[serde(rename = "lockProfileInstalled")]
pub lock_profile_installed: bool,
#[serde(rename = "lastAppPushAt")]
pub last_app_push_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MdmStatusByUdidData {
pub enrolled: bool,
pub company: Option<String>,
pub supervised: bool,
#[serde(rename = "lastAppPushAt")]
pub last_app_push_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MdmLinkRequest {
#[serde(rename = "mdmId")]
pub mdm_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportProtectionStateRequest {
#[serde(rename = "deviceId")]
pub device_id: String,
pub platform: String,
#[serde(rename = "protectionType")]
pub protection_type: String,
pub active: bool,
pub reason: Option<String>,
pub source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiEnvelope<T> {
pub success: bool,
pub data: T,
}
pub struct MagicApiClient {
client: reqwest::Client,
base_url: String,
}
impl MagicApiClient {
pub fn new(config: &AppConfig) -> Self {
Self {
client: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(HTTP_TIMEOUT_SECONDS))
.build()
.expect("reqwest client build"),
base_url: config.backend_base_url.clone(),
}
}
pub async fn redeem_pairing_code(
&self,
code: &str,
label: Option<&str>,
) -> AppResult<RedeemPairingResponse> {
let url = format!("{}/api/magic/pair/redeem", self.base_url);
let body = RedeemPairingRequest {
code: code.to_string(),
label: label.map(|s| s.to_string()),
};
let response = self
.client
.post(&url)
.json(&body)
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
let status = response.status();
if status == reqwest::StatusCode::BAD_REQUEST || status == reqwest::StatusCode::NOT_FOUND {
return Err(AppError::new(
"Code ungültig. Bitte in der ReBreak-App einen neuen Code generieren.".to_string(),
));
}
if status == reqwest::StatusCode::GONE {
let text = response
.text()
.await
.unwrap_or_else(|_| "".to_string());
if text.contains("verwendet") {
return Err(AppError::new("Code wurde bereits verwendet.".to_string()));
}
return Err(AppError::new("Code abgelaufen. Bitte einen neuen generieren.".to_string()));
}
Self::handle_response::<ApiEnvelope<RedeemPairingResponse>>(response)
.await
.map(|envelope| envelope.data)
}
pub async fn register_device(
&self,
token: &str,
device_id: Option<&str>,
hardware_id: Option<&str>,
hostname: &str,
model: Option<&str>,
os_version: Option<&str>,
) -> AppResult<RegisterDeviceResponse> {
let url = format!("{}/api/magic/register", self.base_url);
let body = RegisterDeviceRequest {
device_id: device_id.map(|s| s.to_string()),
hardware_id: hardware_id.map(|s| s.to_string()),
hostname: hostname.to_string(),
model: model.map(|s| s.to_string()),
os_version: os_version.map(|s| s.to_string()),
};
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.json(&body)
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
let status = response.status();
if status == reqwest::StatusCode::CONFLICT {
return Err(AppError::new(
"Device-Limit erreicht. Bitte zuerst ein Gerät freigeben.".to_string(),
));
}
Self::handle_response::<ApiEnvelope<RegisterDeviceResponse>>(response)
.await
.map(|envelope| envelope.data)
}
pub async fn list_devices(&self, token: &str) -> AppResult<Vec<MagicDeviceInfo>> {
let url = format!("{}/api/magic/devices", self.base_url);
let response = self
.client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<Vec<MagicDeviceInfo>>>(response)
.await
.map(|envelope| envelope.data)
}
pub async fn get_status(&self, dns_token: &str) -> AppResult<bool> {
let url = format!("{}/api/magic/status", self.base_url);
let response = self
.client
.get(&url)
.query(&[("token", dns_token)])
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<StatusResponse>>(response)
.await
.map(|envelope| envelope.data.active)
}
pub async fn request_release(&self, token: &str, device_id: &str) -> AppResult<ReleaseResponse> {
let url = format!(
"{}/api/magic/devices/{}/request-release",
self.base_url, device_id
);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<ReleaseResponse>>(response)
.await
.map(|envelope| envelope.data)
}
pub async fn cancel_release(&self, token: &str, device_id: &str) -> AppResult<()> {
let url = format!(
"{}/api/magic/devices/{}/cancel-release",
self.base_url, device_id
);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<serde_json::Value>>(response)
.await
.map(|_| ())
}
pub async fn start_cooldown(
&self,
token: &str,
device_id: &str,
duration_minutes: u32,
) -> AppResult<CooldownResponse> {
let url = format!("{}/api/magic/devices/{}/cooldown", self.base_url, device_id);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.json(&serde_json::json!({ "durationMinutes": duration_minutes }))
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<CooldownResponse>>(response)
.await
.map(|envelope| envelope.data)
}
pub async fn cancel_cooldown(&self, token: &str, device_id: &str) -> AppResult<()> {
let url = format!("{}/api/magic/devices/{}/cancel-cooldown", self.base_url, device_id);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<serde_json::Value>>(response)
.await
.map(|_| ())
}
pub async fn fetch_me(&self, token: &str) -> AppResult<UserProfile> {
let url = format!("{}/api/magic/me", self.base_url);
let response = self
.client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<UserProfile>>(response)
.await
.map(|envelope| envelope.data)
}
pub async fn download_profile(&self, dns_token: &str) -> AppResult<Vec<u8>> {
let url = format!(
"{}/api/magic/profile.mobileconfig?token={}",
self.base_url, dns_token
);
let response = self
.client
.get(&url)
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
return Err(AppError::new(format!("HTTP {}: {}", status, text)));
}
response
.bytes()
.await
.map(|b| b.to_vec())
.map_err(|e| AppError::new(format!("Failed to read profile: {}", e)))
}
pub async fn get_mdm_status(&self, token: &str, device_id: &str) -> AppResult<MdmStatusData> {
let url = format!("{}/api/magic/devices/{}/mdm", self.base_url, device_id);
let response = self
.client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<MdmStatusData>>(response)
.await
.map(|envelope| envelope.data)
}
pub async fn get_mdm_status_by_udid(
&self,
token: &str,
udid: &str,
) -> AppResult<MdmStatusByUdidData> {
let url = format!("{}/api/magic/mdm/by-udid", self.base_url);
let response = self
.client
.get(&url)
.header("Authorization", format!("Bearer {}", token))
.query(&[("udid", udid)])
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<MdmStatusByUdidData>>(response)
.await
.map(|envelope| envelope.data)
}
pub async fn link_mdm_device(
&self,
token: &str,
device_id: &str,
mdm_id: &str,
) -> AppResult<()> {
let url = format!(
"{}/api/magic/devices/{}/mdm-link",
self.base_url, device_id
);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.json(&MdmLinkRequest {
mdm_id: mdm_id.to_string(),
})
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<serde_json::Value>>(response)
.await
.map(|_| ())
}
pub async fn report_device_protection_state(
&self,
token: &str,
device_id: &str,
platform: &str,
protection_type: &str,
active: bool,
reason: Option<&str>,
) -> AppResult<()> {
let url = format!("{}/api/devices/protection-state", self.base_url);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", token))
.json(&ReportProtectionStateRequest {
device_id: device_id.to_string(),
platform: platform.to_string(),
protection_type: protection_type.to_string(),
active,
reason: reason.map(|s| s.to_string()),
source: Some("magic".to_string()),
})
.send()
.await
.map_err(|e| AppError::new(format!("Network error: {}", e)))?;
Self::handle_response::<ApiEnvelope<serde_json::Value>>(response)
.await
.map(|_| ())
}
async fn handle_response<T: serde::de::DeserializeOwned>(
response: reqwest::Response,
) -> AppResult<T> {
let status = response.status();
let body = response
.text()
.await
.unwrap_or_else(|_| "<failed to read body>".to_string());
if status.is_success() {
serde_json::from_str::<T>(&body).map_err(|e| {
AppError::new(format!(
"Failed to parse response: {} | body={}",
e, body
))
})
} else {
Err(AppError::new(format!("HTTP {}: {}", status, body)))
}
}
}