feat(magic): expose cooldown commands and extend device types
This commit is contained in:
parent
97977011ae
commit
8f5e34ae67
284
apps/rebreak-magic/app/composables/useTauri.ts
Normal file
284
apps/rebreak-magic/app/composables/useTauri.ts
Normal file
@ -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<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
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<T>(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<PlatformInfo> {
|
||||
return await invokeLogged("get_platform");
|
||||
}
|
||||
|
||||
async function redeemPairingCode(code: string, label?: string): Promise<RedeemPairingResponse> {
|
||||
return await invokeLogged("redeem_pairing_code", { code, label });
|
||||
}
|
||||
|
||||
async function registerDevice(
|
||||
deviceId: string,
|
||||
model: string | undefined,
|
||||
osVersion: string | undefined,
|
||||
): Promise<RegisterDeviceResponse> {
|
||||
return await invokeLogged("register_device", { deviceId, model, osVersion });
|
||||
}
|
||||
|
||||
async function getStoredSession(): Promise<MagicSession | null> {
|
||||
return await invokeLogged("get_stored_session");
|
||||
}
|
||||
|
||||
async function getMagicDevices(): Promise<MagicDeviceInfo[]> {
|
||||
return await invokeLogged("get_magic_devices");
|
||||
}
|
||||
|
||||
async function getMagicStatus(dnsToken: string): Promise<boolean> {
|
||||
return await invokeLogged("get_magic_status", { dnsToken });
|
||||
}
|
||||
|
||||
async function requestRelease(deviceId: string): Promise<ReleaseResponse> {
|
||||
return await invokeLogged("request_release", { deviceId });
|
||||
}
|
||||
|
||||
async function cancelRelease(deviceId: string): Promise<void> {
|
||||
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<void> {
|
||||
await invokeLogged("cancel_cooldown", { deviceId });
|
||||
}
|
||||
|
||||
async function logoutMagic(): Promise<void> {
|
||||
return await invokeLogged("logout_magic");
|
||||
}
|
||||
|
||||
async function startLocalProfileServer(profilePath: string): Promise<LocalServerInfo> {
|
||||
return await invokeLogged("start_local_profile_server", { profilePath });
|
||||
}
|
||||
|
||||
async function stopLocalProfileServer(): Promise<void> {
|
||||
await invokeLogged("stop_local_profile_server");
|
||||
}
|
||||
|
||||
async function runSuperviseMagic(action: string, args?: string[]): Promise<SuperviseResult> {
|
||||
return await invokeLogged("run_supervise_magic", { action, args });
|
||||
}
|
||||
|
||||
async function downloadProfile(profileUrl: string): Promise<string> {
|
||||
return await invokeLogged("download_profile", { profileUrl });
|
||||
}
|
||||
|
||||
async function activateProtection(profilePath: string): Promise<void> {
|
||||
return await invokeLogged("activate_protection", { profilePath });
|
||||
}
|
||||
|
||||
async function fetchMe(): Promise<UserProfile> {
|
||||
return await invokeLogged("fetch_me");
|
||||
}
|
||||
|
||||
async function detectIphoneState(): Promise<IphoneDeviceState | null> {
|
||||
return await invokeLogged("detect_iphone_state");
|
||||
}
|
||||
|
||||
async function getSuperviseStatus(): Promise<SuperviseStatus> {
|
||||
return await invokeLogged("get_supervise_status");
|
||||
}
|
||||
|
||||
async function getInstalledProfiles(): Promise<string[]> {
|
||||
return await invokeLogged("get_installed_profiles");
|
||||
}
|
||||
|
||||
async function getInstalledApps(): Promise<string[]> {
|
||||
return await invokeLogged("get_installed_apps");
|
||||
}
|
||||
|
||||
async function installProfile(path: string): Promise<SuperviseResult> {
|
||||
return await invokeLogged("install_profile", { path });
|
||||
}
|
||||
|
||||
async function downloadAndPatchEnrollmentProfile(url: string, udid: string): Promise<string> {
|
||||
return await invokeLogged("download_and_patch_enrollment_profile", { url, udid });
|
||||
}
|
||||
|
||||
async function mdmPing(): Promise<string> {
|
||||
return await invokeLogged("mdm_ping");
|
||||
}
|
||||
|
||||
async function mdmPush(udid: string): Promise<MdmPushStatus> {
|
||||
return await invokeLogged("mdm_push", { udid });
|
||||
}
|
||||
|
||||
async function mdmInstallApp(udid: string): Promise<MdmCommandResult> {
|
||||
return await invokeLogged("mdm_install_app", { udid });
|
||||
}
|
||||
|
||||
async function mdmSetSupervisedMode(udid: string): Promise<MdmCommandResult> {
|
||||
return await invokeLogged("mdm_set_supervised_mode", { udid });
|
||||
}
|
||||
|
||||
async function mdmTakeManagement(udid: string): Promise<MdmCommandResult> {
|
||||
return await invokeLogged("mdm_take_management", { udid });
|
||||
}
|
||||
|
||||
async function mdmInstallLockProfile(udid: string, profilePath: string): Promise<MdmCommandResult> {
|
||||
return await invokeLogged("mdm_install_lock_profile", { udid, profilePath });
|
||||
}
|
||||
|
||||
async function getDesktopProtectionStatus(): Promise<DesktopProtectionState | null> {
|
||||
return await invokeLogged("get_desktop_protection_status");
|
||||
}
|
||||
|
||||
async function setDesktopProtectionStatus(active: boolean, platform: string): Promise<void> {
|
||||
await invokeLogged("set_desktop_protection_status", { active, platform });
|
||||
}
|
||||
|
||||
async function getHostname(): Promise<string> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
354
apps/rebreak-magic/src-tauri/src/backend/api.rs
Normal file
354
apps/rebreak-magic/src-tauri/src/backend/api.rs
Normal file
@ -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<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")]
|
||||
pub device_id: 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,
|
||||
pub hostname: String,
|
||||
pub model: Option<String>,
|
||||
#[serde(rename = "osVersion")]
|
||||
pub os_version: 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(rename = "cooldownUntil")]
|
||||
pub cooldown_until: Option<String>,
|
||||
pub status: String,
|
||||
#[serde(rename = "lastSeenAt")]
|
||||
pub last_seen_at: Option<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 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: &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.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)))
|
||||
}
|
||||
|
||||
async fn handle_response<T: serde::de::DeserializeOwned>(
|
||||
response: reqwest::Response,
|
||||
) -> AppResult<T> {
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
response
|
||||
.json::<T>()
|
||||
.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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
234
apps/rebreak-magic/src-tauri/src/lib.rs
Normal file
234
apps/rebreak-magic/src-tauri/src/lib.rs
Normal file
@ -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<String> {
|
||||
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<MagicSession> {
|
||||
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<String>) -> AppResult<RedeemPairingResponse> {
|
||||
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<String>,
|
||||
os_version: Option<String>,
|
||||
) -> AppResult<RegisterDeviceResponse> {
|
||||
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<Option<MagicSession>> {
|
||||
AppConfig::load_magic_session()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_magic_devices() -> AppResult<Vec<MagicDeviceInfo>> {
|
||||
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<bool> {
|
||||
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<ReleaseResponse> {
|
||||
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<serde_json::Value> {
|
||||
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<UserProfile> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
profile_url.split("token=").nth(1).map(|s| s.split('&').next().unwrap_or(s).to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_desktop_protection_status() -> AppResult<Option<DesktopProtectionState>> {
|
||||
AppConfig::load_desktop_protection()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_hostname() -> AppResult<String> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user