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.
504 lines
16 KiB
Rust
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)))
|
|
}
|
|
}
|
|
}
|