From 75d1b0610578b067d26bea491415a52dce157365 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Wed, 17 Jun 2026 23:32:41 +0200 Subject: [PATCH] feat(magic): iOS device card warning badge, USB hint, split backend/local cards and auto-sync --- .../app/components/IosDeviceCard.vue | 486 +++++++++++++++--- .../app/composables/useMdmStatus.ts | 65 +++ .../rebreak-magic/app/composables/useTauri.ts | 18 + .../src-tauri/src/backend/api.rs | 60 +++ apps/rebreak-magic/src-tauri/src/lib.rs | 22 +- 5 files changed, 571 insertions(+), 80 deletions(-) create mode 100644 apps/rebreak-magic/app/composables/useMdmStatus.ts diff --git a/apps/rebreak-magic/app/components/IosDeviceCard.vue b/apps/rebreak-magic/app/components/IosDeviceCard.vue index 3064e09..cac9abb 100644 --- a/apps/rebreak-magic/app/components/IosDeviceCard.vue +++ b/apps/rebreak-magic/app/components/IosDeviceCard.vue @@ -25,66 +25,150 @@ - {{ statusLabel }} + {{ topBadge.label }} +
-
- - iOS Schutz-Status - - - Verbunden - -
- -
    -
  • - Supervised - - {{ iosStars.isSupervised ? "Ja" : "Nein" }} - -
  • -
  • - Enrollment - - {{ iosStars.enrollment ? "Ja" : "Nein" }} - -
  • -
  • - Sideload/Lock - - {{ iosStars.sideload ? "Ja" : "Nein" }} - -
  • -
  • - ReBreak App - - {{ iosStars.app ? "Ja" : "Nein" }} - -
  • -
+
+

+ Schutz unvollständig +

+

+ {{ incompleteMessage }} +

+
+ +
+ + Verbinde dein iPhone mit USB, um den Schutz zu vervollständigen. +
+ +
- - Zum Live-Status iPhone per USB verbinden und aktualisieren + +
+ +

+ Schutz wird geprüft… +

+

+ Backend- und USB-Status werden abgeglichen +

+
+ + +
+
+ + Backend-MDM + + + Lädt… + + + Enrolled + + + Nicht enrolled + +
+ +
    +
  • + {{ row.label }} + {{ row.value }} +
  • +
+
+ + +
+
+ + Lokales USB-Gerät + + + Verbunden + +
+ +
    +
  • + {{ row.label }} + {{ row.value }} +
  • +
+
+
+ + +
+

+ {{ mismatches.length }} Unterschied(e) erkannt +

+
@@ -104,7 +188,8 @@ :variant="action.variant" size="sm" :icon="action.icon" - :loading="syncing" + :loading="manualSyncing || autoSyncing" + :disabled="autoSyncing" @click="onActionClick" > {{ action.label }} @@ -126,8 +211,9 @@ diff --git a/apps/rebreak-magic/app/composables/useMdmStatus.ts b/apps/rebreak-magic/app/composables/useMdmStatus.ts new file mode 100644 index 0000000..b588fc3 --- /dev/null +++ b/apps/rebreak-magic/app/composables/useMdmStatus.ts @@ -0,0 +1,65 @@ +import { ref, watch, type Ref } from "vue"; +import { useTauri, type MdmStatusData } from "./useTauri"; + +export interface MdmStatusState { + data: MdmStatusData | null; + loading: boolean; + error: string | null; +} + +export function useMdmStatus(deviceId: Ref) { + const { getMdmStatus, linkMdmDevice } = useTauri(); + + const state = ref({ + data: null, + loading: false, + error: null, + }); + + async function refresh() { + const id = deviceId.value; + if (!id) { + state.value.data = null; + return; + } + + state.value.loading = true; + state.value.error = null; + try { + state.value.data = await getMdmStatus(id); + } catch (e: any) { + state.value.error = e?.message ?? "MDM-Status konnte nicht geladen werden"; + state.value.data = null; + } finally { + state.value.loading = false; + } + } + + async function link(mdmId: string) { + const id = deviceId.value; + if (!id) return; + + state.value.loading = true; + state.value.error = null; + try { + await linkMdmDevice(id, mdmId); + await refresh(); + } catch (e: any) { + state.value.error = e?.message ?? "MDM-Verknüpfung fehlgeschlagen"; + } finally { + state.value.loading = false; + } + } + + watch( + () => deviceId.value, + () => refresh(), + { immediate: true }, + ); + + return { + state, + refresh, + link, + }; +} diff --git a/apps/rebreak-magic/app/composables/useTauri.ts b/apps/rebreak-magic/app/composables/useTauri.ts index 6e103db..a99473a 100644 --- a/apps/rebreak-magic/app/composables/useTauri.ts +++ b/apps/rebreak-magic/app/composables/useTauri.ts @@ -82,6 +82,14 @@ export interface DesktopProtectionState { activatedAt: string; } +export interface MdmStatusData { + enrolled: boolean; + company: string | null; + supervised: boolean; + lockProfileInstalled: boolean; + lastAppPushAt: string | null; +} + export interface SuperviseStatus { isSupervised: boolean; organizationName?: string; @@ -262,6 +270,14 @@ export function useTauri() { return await invokeLogged("get_hostname"); } + async function getMdmStatus(deviceId: string): Promise { + return await invokeLogged("get_mdm_status", { deviceId }); + } + + async function linkMdmDevice(deviceId: string, mdmId: string): Promise { + await invokeLogged("link_mdm_device", { deviceId, mdmId }); + } + return { getPlatform, redeemPairingCode, @@ -297,5 +313,7 @@ export function useTauri() { getHostname, getHardwareId, getDeviceId, + getMdmStatus, + linkMdmDevice, }; } diff --git a/apps/rebreak-magic/src-tauri/src/backend/api.rs b/apps/rebreak-magic/src-tauri/src/backend/api.rs index 3c9c250..a8289a1 100644 --- a/apps/rebreak-magic/src-tauri/src/backend/api.rs +++ b/apps/rebreak-magic/src-tauri/src/backend/api.rs @@ -97,6 +97,23 @@ pub struct UserProfile { 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 MdmLinkRequest { + #[serde(rename = "mdmId")] + pub mdm_id: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApiEnvelope { pub success: bool, @@ -345,6 +362,49 @@ impl MagicApiClient { .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 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(|_| ()) + } + async fn handle_response( response: reqwest::Response, ) -> AppResult { diff --git a/apps/rebreak-magic/src-tauri/src/lib.rs b/apps/rebreak-magic/src-tauri/src/lib.rs index 1c09a4e..89f0929 100644 --- a/apps/rebreak-magic/src-tauri/src/lib.rs +++ b/apps/rebreak-magic/src-tauri/src/lib.rs @@ -8,8 +8,8 @@ mod server; mod sidecar; use backend::api::{ - MagicApiClient, MagicDeviceInfo, RedeemPairingResponse, RegisterDeviceResponse, ReleaseResponse, - UserProfile, + MagicApiClient, MagicDeviceInfo, MdmStatusData, RedeemPairingResponse, RegisterDeviceResponse, + ReleaseResponse, UserProfile, }; use config::{AppConfig, DesktopProtectionState, MagicSession}; use error::AppResult; @@ -51,6 +51,8 @@ pub fn run() { download_profile, activate_protection, fetch_me, + get_mdm_status, + link_mdm_device, get_desktop_protection_status, set_desktop_protection_status, get_hostname, @@ -217,6 +219,22 @@ async fn fetch_me() -> AppResult { client.fetch_me(&session.access_token).await } +#[tauri::command] +async fn get_mdm_status(device_id: String) -> AppResult { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + client.get_mdm_status(&session.access_token, &device_id).await +} + +#[tauri::command] +async fn link_mdm_device(device_id: String, mdm_id: String) -> AppResult<()> { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + client.link_mdm_device(&session.access_token, &device_id, &mdm_id).await +} + #[tauri::command] async fn download_profile(profile_url: String) -> AppResult { let session = require_session()?;