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, } #[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, #[serde(rename = "hardwareId", skip_serializing_if = "Option::is_none")] pub hardware_id: Option, pub hostname: String, pub model: Option, #[serde(rename = "osVersion")] pub os_version: Option, } #[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, pub hostname: String, pub model: Option, #[serde(rename = "osVersion")] pub os_version: Option, #[serde(default, rename = "mdmId")] pub mdm_id: Option, #[serde(rename = "magicEnrolledAt")] pub magic_enrolled_at: Option, #[serde(rename = "releaseRequestedAt")] pub release_requested_at: Option, #[serde(rename = "releaseAvailableAt")] pub release_available_at: Option, #[serde(default, rename = "cooldownUntil")] pub cooldown_until: Option, #[serde(default = "default_active_status")] pub status: String, #[serde(default, rename = "lastSeenAt")] pub last_seen_at: Option, } 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, pub avatar: Option, pub plan: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MdmStatusData { pub enrolled: bool, pub company: Option, pub supervised: bool, #[serde(rename = "lockProfileInstalled")] pub lock_profile_installed: bool, #[serde(rename = "lastAppPushAt")] pub last_app_push_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MdmStatusByUdidData { pub enrolled: bool, pub company: Option, pub supervised: bool, #[serde(rename = "lastAppPushAt")] pub last_app_push_at: Option, } #[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, pub source: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApiEnvelope { 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 { 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::>(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 { 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::>(response) .await .map(|envelope| envelope.data) } pub async fn list_devices(&self, token: &str) -> AppResult> { 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::>>(response) .await .map(|envelope| envelope.data) } pub async fn get_status(&self, dns_token: &str) -> AppResult { 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::>(response) .await .map(|envelope| envelope.data.active) } pub async fn request_release(&self, token: &str, device_id: &str) -> AppResult { 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::>(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::>(response) .await .map(|_| ()) } pub async fn start_cooldown( &self, token: &str, device_id: &str, duration_minutes: u32, ) -> AppResult { 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::>(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::>(response) .await .map(|_| ()) } pub async fn fetch_me(&self, token: &str) -> AppResult { 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::>(response) .await .map(|envelope| envelope.data) } pub async fn download_profile(&self, dns_token: &str) -> AppResult> { 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 { 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::>(response) .await .map(|envelope| envelope.data) } pub async fn get_mdm_status_by_udid( &self, token: &str, udid: &str, ) -> AppResult { 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::>(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::>(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::>(response) .await .map(|_| ()) } async fn handle_response( response: reqwest::Response, ) -> AppResult { let status = response.status(); let body = response .text() .await .unwrap_or_else(|_| "".to_string()); if status.is_success() { serde_json::from_str::(&body).map_err(|e| { AppError::new(format!( "Failed to parse response: {} | body={}", e, body )) }) } else { Err(AppError::new(format!("HTTP {}: {}", status, body))) } } }