From 8f5e34ae67349c477c16b2467ae18708fa5cb1c3 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Tue, 16 Jun 2026 20:33:38 +0200 Subject: [PATCH] feat(magic): expose cooldown commands and extend device types --- .../rebreak-magic/app/composables/useTauri.ts | 284 ++++++++++++++ .../src-tauri/src/backend/api.rs | 354 ++++++++++++++++++ apps/rebreak-magic/src-tauri/src/lib.rs | 234 ++++++++++++ 3 files changed, 872 insertions(+) create mode 100644 apps/rebreak-magic/app/composables/useTauri.ts create mode 100644 apps/rebreak-magic/src-tauri/src/backend/api.rs create mode 100644 apps/rebreak-magic/src-tauri/src/lib.rs diff --git a/apps/rebreak-magic/app/composables/useTauri.ts b/apps/rebreak-magic/app/composables/useTauri.ts new file mode 100644 index 0000000..2e7e59e --- /dev/null +++ b/apps/rebreak-magic/app/composables/useTauri.ts @@ -0,0 +1,284 @@ +import { invoke } from "@tauri-apps/api/core"; + +export interface PlatformInfo { + platform: "MacOS" | "Windows" | "Linux" | "Unknown"; + version: string; + supports_ios_supervision: boolean; +} + +export interface LocalServerInfo { + url: string; + qr_payload: string; +} + +export interface SuperviseResult { + success: boolean; + stdout: string; + stderr: string; +} + +export interface RedeemPairingResponse { + token: string; + sessionId: string; + createdAt: string; +} + +export interface MagicSession { + accessToken: string; + sessionId: string; + userId: string; + createdAt: number; + label: string | null; +} + +export interface RegisterDeviceResponse { + deviceId: string; + dnsToken: string; + profileUrl: string; + existing: boolean; +} + +export interface MagicDeviceInfo { + source: string; + deviceId: string; + hostname: string; + model: string | null; + osVersion: string | null; + magicEnrolledAt: string | null; + releaseRequestedAt: string | null; + releaseAvailableAt: string | null; + cooldownUntil: string | null; + status: "active" | "cooldown" | "revoked" | "pending"; + lastSeenAt: string | null; +} + +export interface ReleaseResponse { + releaseRequestedAt: string; + releaseAvailableAt: string; +} + +export interface UserProfile { + nickname: string | null; + avatar: string | null; + plan: string | null; +} + +export interface IphoneDeviceState { + udid: string; + name: string; + productType: string; + productVersion: string; + isSupervised: boolean; + organizationName?: string; + findMyEnabled?: boolean; + installedProfileIDs: string[]; + installedAppBundleIDs: string[]; +} + +export interface DesktopProtectionState { + active: boolean; + platform: string; + activatedAt: string; +} + +export interface SuperviseStatus { + isSupervised: boolean; + organizationName?: string; + findMyEnabled?: boolean; +} + +export interface MdmPushStatus { + udid: string; + push_result: string; +} + +export interface MdmCommandResult { + command_uuid: string; + response_body: string; +} + +async function invokeLogged(command: string, args?: Record): Promise { + const { useLogger } = await import("~/composables/useLogger"); + const logger = useLogger(); + + try { + logger.info(`invoke:${command}`, args ? JSON.stringify(args, null, 2) : undefined); + const result = await invoke(command, args); + logger.debug(`invoke:${command}:ok`, typeof result === "object" ? JSON.stringify(result, null, 2) : String(result)); + return result; + } catch (err) { + const { formatError } = await import("~/composables/useLogger"); + const formatted = formatError(err); + logger.error(`invoke:${command}:failed`, formatted.details ? `${formatted.message}\n${formatted.details}` : formatted.message); + throw err; + } +} + +export function useTauri() { + async function getPlatform(): Promise { + return await invokeLogged("get_platform"); + } + + async function redeemPairingCode(code: string, label?: string): Promise { + return await invokeLogged("redeem_pairing_code", { code, label }); + } + + async function registerDevice( + deviceId: string, + model: string | undefined, + osVersion: string | undefined, + ): Promise { + return await invokeLogged("register_device", { deviceId, model, osVersion }); + } + + async function getStoredSession(): Promise { + return await invokeLogged("get_stored_session"); + } + + async function getMagicDevices(): Promise { + return await invokeLogged("get_magic_devices"); + } + + async function getMagicStatus(dnsToken: string): Promise { + return await invokeLogged("get_magic_status", { dnsToken }); + } + + async function requestRelease(deviceId: string): Promise { + return await invokeLogged("request_release", { deviceId }); + } + + async function cancelRelease(deviceId: string): Promise { + return await invokeLogged("cancel_release", { deviceId }); + } + + async function startCooldown(deviceId: string, durationMinutes: number = 60): Promise<{ cooldownUntil: string }> { + return await invokeLogged("start_cooldown", { deviceId, durationMinutes }); + } + + async function cancelCooldown(deviceId: string): Promise { + await invokeLogged("cancel_cooldown", { deviceId }); + } + + async function logoutMagic(): Promise { + return await invokeLogged("logout_magic"); + } + + async function startLocalProfileServer(profilePath: string): Promise { + return await invokeLogged("start_local_profile_server", { profilePath }); + } + + async function stopLocalProfileServer(): Promise { + await invokeLogged("stop_local_profile_server"); + } + + async function runSuperviseMagic(action: string, args?: string[]): Promise { + return await invokeLogged("run_supervise_magic", { action, args }); + } + + async function downloadProfile(profileUrl: string): Promise { + return await invokeLogged("download_profile", { profileUrl }); + } + + async function activateProtection(profilePath: string): Promise { + return await invokeLogged("activate_protection", { profilePath }); + } + + async function fetchMe(): Promise { + return await invokeLogged("fetch_me"); + } + + async function detectIphoneState(): Promise { + return await invokeLogged("detect_iphone_state"); + } + + async function getSuperviseStatus(): Promise { + return await invokeLogged("get_supervise_status"); + } + + async function getInstalledProfiles(): Promise { + return await invokeLogged("get_installed_profiles"); + } + + async function getInstalledApps(): Promise { + return await invokeLogged("get_installed_apps"); + } + + async function installProfile(path: string): Promise { + return await invokeLogged("install_profile", { path }); + } + + async function downloadAndPatchEnrollmentProfile(url: string, udid: string): Promise { + return await invokeLogged("download_and_patch_enrollment_profile", { url, udid }); + } + + async function mdmPing(): Promise { + return await invokeLogged("mdm_ping"); + } + + async function mdmPush(udid: string): Promise { + return await invokeLogged("mdm_push", { udid }); + } + + async function mdmInstallApp(udid: string): Promise { + return await invokeLogged("mdm_install_app", { udid }); + } + + async function mdmSetSupervisedMode(udid: string): Promise { + return await invokeLogged("mdm_set_supervised_mode", { udid }); + } + + async function mdmTakeManagement(udid: string): Promise { + return await invokeLogged("mdm_take_management", { udid }); + } + + async function mdmInstallLockProfile(udid: string, profilePath: string): Promise { + return await invokeLogged("mdm_install_lock_profile", { udid, profilePath }); + } + + async function getDesktopProtectionStatus(): Promise { + return await invokeLogged("get_desktop_protection_status"); + } + + async function setDesktopProtectionStatus(active: boolean, platform: string): Promise { + await invokeLogged("set_desktop_protection_status", { active, platform }); + } + + async function getHostname(): Promise { + return await invokeLogged("get_hostname"); + } + + return { + getPlatform, + redeemPairingCode, + registerDevice, + getStoredSession, + getMagicDevices, + getMagicStatus, + requestRelease, + cancelRelease, + startCooldown, + cancelCooldown, + logoutMagic, + startLocalProfileServer, + stopLocalProfileServer, + runSuperviseMagic, + downloadProfile, + activateProtection, + fetchMe, + detectIphoneState, + getSuperviseStatus, + getInstalledProfiles, + getInstalledApps, + installProfile, + downloadAndPatchEnrollmentProfile, + mdmPing, + mdmPush, + mdmInstallApp, + mdmSetSupervisedMode, + mdmTakeManagement, + mdmInstallLockProfile, + getDesktopProtectionStatus, + setDesktopProtectionStatus, + getHostname, + }; +} diff --git a/apps/rebreak-magic/src-tauri/src/backend/api.rs b/apps/rebreak-magic/src-tauri/src/backend/api.rs new file mode 100644 index 0000000..468917d --- /dev/null +++ b/apps/rebreak-magic/src-tauri/src/backend/api.rs @@ -0,0 +1,354 @@ +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")] + pub device_id: String, + 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, + pub hostname: String, + pub model: Option, + #[serde(rename = "osVersion")] + pub os_version: 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(rename = "cooldownUntil")] + pub cooldown_until: Option, + pub status: String, + #[serde(rename = "lastSeenAt")] + pub last_seen_at: Option, +} + +#[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 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: &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.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))) + } + + async fn handle_response( + response: reqwest::Response, + ) -> AppResult { + let status = response.status(); + if status.is_success() { + response + .json::() + .await + .map_err(|e| AppError::new(format!("Failed to parse response: {}", e))) + } else { + let text = response + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + Err(AppError::new(format!("HTTP {}: {}", status, text))) + } + } +} diff --git a/apps/rebreak-magic/src-tauri/src/lib.rs b/apps/rebreak-magic/src-tauri/src/lib.rs new file mode 100644 index 0000000..2da0291 --- /dev/null +++ b/apps/rebreak-magic/src-tauri/src/lib.rs @@ -0,0 +1,234 @@ +mod backend; +mod config; +mod error; +mod ios_device; +mod mdm; +mod platform; +mod server; +mod sidecar; + +use backend::api::{ + MagicApiClient, MagicDeviceInfo, RedeemPairingResponse, RegisterDeviceResponse, ReleaseResponse, + UserProfile, +}; +use config::{AppConfig, DesktopProtectionState, MagicSession}; +use error::AppResult; +use std::process::Command; + +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_os::init()) + .invoke_handler(tauri::generate_handler![ + platform::get_platform, + server::local_http::start_local_profile_server, + server::local_http::stop_local_profile_server, + sidecar::supervise_magic::run_supervise_magic, + ios_device::detect_iphone_state, + ios_device::get_supervise_status, + ios_device::get_installed_profiles, + ios_device::get_installed_apps, + ios_device::install_profile, + ios_device::download_and_patch_enrollment_profile, + mdm::mdm_ping, + mdm::mdm_push, + mdm::mdm_install_app, + mdm::mdm_set_supervised_mode, + mdm::mdm_take_management, + mdm::mdm_install_lock_profile, + redeem_pairing_code, + register_device, + get_stored_session, + get_magic_devices, + get_magic_status, + request_release, + cancel_release, + start_cooldown, + cancel_cooldown, + logout_magic, + download_profile, + activate_protection, + fetch_me, + get_desktop_protection_status, + set_desktop_protection_status, + get_hostname, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +fn hostname() -> AppResult { + let output = Command::new("hostname") + .output() + .map_err(|e| error::AppError::new(format!("Failed to get hostname: {}", e)))?; + let mut hostname = String::from_utf8_lossy(&output.stdout).to_string(); + hostname = hostname.trim().to_string(); + if hostname.is_empty() { + hostname = "ReBreak Magic Device".to_string(); + } + Ok(hostname) +} + +fn require_session() -> AppResult { + AppConfig::load_magic_session()?.ok_or_else(|| { + error::AppError::new("Keine Magic-Session gefunden. Bitte neu paaren.") + }) +} + +#[tauri::command] +async fn redeem_pairing_code(code: String, label: Option) -> AppResult { + let trimmed = code.trim().to_string(); + if !trimmed.chars().all(|c| c.is_ascii_digit()) || trimmed.len() != 6 { + return Err(error::AppError::new("Der Code besteht aus 6 Ziffern.".to_string())); + } + + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + let response = client + .redeem_pairing_code(&trimmed, label.as_deref()) + .await?; + + let label = label.unwrap_or_else(|| hostname().unwrap_or_else(|_| "ReBreak Magic".to_string())); + let session = MagicSession::new(response.token.clone(), response.session_id.clone(), Some(label)); + AppConfig::save_magic_session(&session)?; + + Ok(response) +} + +#[tauri::command] +async fn register_device( + device_id: String, + model: Option, + os_version: Option, +) -> AppResult { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + let hostname = hostname()?; + let response = client + .register_device(&session.access_token, &device_id, &hostname, model.as_deref(), os_version.as_deref()) + .await?; + Ok(response) +} + +#[tauri::command] +async fn get_stored_session() -> AppResult> { + AppConfig::load_magic_session() +} + +#[tauri::command] +async fn get_magic_devices() -> AppResult> { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + client.list_devices(&session.access_token).await +} + +#[tauri::command] +async fn get_magic_status(dns_token: String) -> AppResult { + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + client.get_status(&dns_token).await +} + +#[tauri::command] +async fn request_release(device_id: String) -> AppResult { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + client.request_release(&session.access_token, &device_id).await +} + +#[tauri::command] +async fn cancel_release(device_id: String) -> AppResult<()> { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + client.cancel_release(&session.access_token, &device_id).await +} + +#[tauri::command] +async fn start_cooldown(device_id: String, duration_minutes: u32) -> AppResult { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + let result = client.start_cooldown(&session.access_token, &device_id, duration_minutes).await?; + Ok(serde_json::json!({ "cooldownUntil": result.cooldown_until })) +} + +#[tauri::command] +async fn cancel_cooldown(device_id: String) -> AppResult<()> { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + client.cancel_cooldown(&session.access_token, &device_id).await +} + +#[tauri::command] +fn logout_magic() -> AppResult<()> { + AppConfig::clear_magic_session() +} + +#[tauri::command] +fn activate_protection(profile_path: String) -> AppResult<()> { + platform::activate_protection(&profile_path) +} + +#[tauri::command] +async fn fetch_me() -> AppResult { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + client.fetch_me(&session.access_token).await +} + +#[tauri::command] +async fn download_profile(profile_url: String) -> AppResult { + let session = require_session()?; + let config = AppConfig::load(); + let client = MagicApiClient::new(&config); + + // Try to extract dns_token from profileUrl, fallback to direct token lookup + let dns_token = extract_dns_token(&profile_url) + .or_else(|| Some(session.access_token.clone())) + .ok_or_else(|| error::AppError::new("Kein DNS-Token verfügbar".to_string()))?; + + let bytes = client.download_profile(&dns_token).await?; + + let config_dir = AppConfig::config_dir()?; + std::fs::create_dir_all(&config_dir)?; + let profile_path = config_dir.join("rebreak-iphone-protect.mobileconfig"); + std::fs::write(&profile_path, bytes)?; + + Ok(profile_path.to_string_lossy().to_string()) +} + +fn extract_dns_token(profile_url: &str) -> Option { + profile_url.split("token=").nth(1).map(|s| s.split('&').next().unwrap_or(s).to_string()) +} + +#[tauri::command] +fn get_desktop_protection_status() -> AppResult> { + AppConfig::load_desktop_protection() +} + +#[tauri::command] +fn get_hostname() -> AppResult { + hostname() +} + +#[tauri::command] +fn set_desktop_protection_status(active: bool, platform: String) -> AppResult<()> { + if active { + let state = DesktopProtectionState { + active, + platform, + activated_at: chrono::Utc::now(), + }; + AppConfig::save_desktop_protection(&state) + } else { + AppConfig::clear_desktop_protection() + } +}