feat(magic): inline Lock-Profil via QR-Code + lokaler Server

- Umbau von MDM-Push auf QR-Code-basierte lokale Profil-Installation
- Automatischer Übergang Enrollment → Lock-Profil nach erfolgreichem Scan
- Lokaler Server erlaubt nun mehrere aufeinanderfolgende Profil-Starts
- .sixth/ in .gitignore aufgenommen
This commit is contained in:
chahinebrini 2026-06-18 08:24:57 +02:00
parent 5404f6676b
commit 92ab26605f
3 changed files with 97 additions and 23 deletions

3
.gitignore vendored
View File

@ -31,6 +31,9 @@ Thumbs.db
# Claude Code agent state (lokale Definitionen, nicht versioniert) # Claude Code agent state (lokale Definitionen, nicht versioniert)
.claude/ .claude/
# Kimi Code / Sixth plugin state
.sixth/
# xgit binary (generated) # xgit binary (generated)
xgit xgit

View File

@ -195,18 +195,60 @@
<UIcon <UIcon
name="i-heroicons-lock-closed" name="i-heroicons-lock-closed"
class="w-5 h-5 text-purple-600 dark:text-purple-400" class="w-5 h-5 text-purple-600 dark:text-purple-400"
:class="{ 'animate-spin': lockPhase === 'loading' }" :class="{ 'animate-spin': lockPhase === 'loading' || lockPhase === 'checking' }"
/> />
<p class="text-sm font-bold text-purple-900 dark:text-purple-200"> <p class="text-sm font-bold text-purple-900 dark:text-purple-200">
Lock-Profil Lock-Profil
</p> </p>
</div> </div>
<!-- Progress steps -->
<div class="flex items-center gap-2 text-xs mb-4">
<span
class="px-2 py-1 rounded-full"
:class="lockPhase === 'loading' ? 'bg-purple-200 text-purple-800 dark:bg-purple-700 dark:text-purple-100' : 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'"
>
1. Server starten
</span>
<span class="text-gray-400"></span>
<span
class="px-2 py-1 rounded-full"
:class="lockPhase === 'waiting' ? 'bg-purple-200 text-purple-800 dark:bg-purple-700 dark:text-purple-100' : (lockPhase === 'checking' || lockPhase === 'success' || lockPhase === 'error') ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'"
>
2. QR-Code scannen
</span>
<span class="text-gray-400"></span>
<span
class="px-2 py-1 rounded-full"
:class="lockPhase === 'checking' ? 'bg-purple-200 text-purple-800 dark:bg-purple-700 dark:text-purple-100' : lockPhase === 'success' ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300' : 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400'"
>
3. Prüfen
</span>
</div>
<!-- QR code -->
<div v-if="lockPhase === 'waiting' && lockQrUrl" class="text-center space-y-3">
<div class="bg-white p-3 rounded-xl inline-block">
<img :src="lockQrUrl" alt="Lock-Profil QR-Code" class="w-40 h-40">
</div>
<p class="text-xs text-purple-700 dark:text-purple-300">
Scanne den Code mit der iPhone-Kamera und installiere das Lock-Profil.
</p>
<UButton
size="sm"
color="primary"
:loading="lockPhase === 'checking'"
@click="checkInlineLockProfile"
>
Installation prüfen
</UButton>
</div>
<div v-if="lockPhase === 'loading'" class="text-sm text-purple-700 dark:text-purple-300"> <div v-if="lockPhase === 'loading'" class="text-sm text-purple-700 dark:text-purple-300">
Lock-Profil wird per MDM auf das iPhone gepusht Lock-Profil-Server wird gestartet
</div> </div>
<div v-if="lockPhase === 'success'" class="text-sm text-green-700 dark:text-green-300"> <div v-if="lockPhase === 'success'" class="text-sm text-green-700 dark:text-green-300">
Lock-Profil-Installation initiiert. Das Gerät aktualisiert den Schutz in Kürze. Lock-Profil installiert. Das Gerät aktualisiert den Schutz in Kürze.
</div> </div>
<div v-if="lockPhase === 'error'" class="text-sm text-red-700 dark:text-red-300"> <div v-if="lockPhase === 'error'" class="text-sm text-red-700 dark:text-red-300">
{{ lockError || "Lock-Profil-Installation fehlgeschlagen" }} {{ lockError || "Lock-Profil-Installation fehlgeschlagen" }}
@ -283,7 +325,6 @@ const {
stopLocalProfileServer, stopLocalProfileServer,
getInstalledProfiles, getInstalledProfiles,
mdmPush, mdmPush,
mdmInstallLockProfile,
} = useTauri(); } = useTauri();
const enrollmentPhase = ref<"idle" | "loading" | "waiting" | "checking" | "success" | "error">("idle"); const enrollmentPhase = ref<"idle" | "loading" | "waiting" | "checking" | "success" | "error">("idle");
@ -292,9 +333,10 @@ const enrollmentQrUrl = ref<string>("");
const enrollmentError = ref<string | null>(null); const enrollmentError = ref<string | null>(null);
const enrollmentLogs = ref<string[]>([]); const enrollmentLogs = ref<string[]>([]);
const lockPhase = ref<"idle" | "loading" | "success" | "error">("idle"); const lockPhase = ref<"idle" | "loading" | "waiting" | "checking" | "success" | "error">("idle");
const lockError = ref<string | null>(null); const lockError = ref<string | null>(null);
const lockLogs = ref<string[]>([]); const lockLogs = ref<string[]>([]);
const lockQrUrl = ref<string>("");
const LOCK_PROFILE_PATH = "/Users/chahinebrini/mono/rebreak-monorepo/ops/mdm/profiles/rebreak-content-filter-sideload.mobileconfig"; const LOCK_PROFILE_PATH = "/Users/chahinebrini/mono/rebreak-monorepo/ops/mdm/profiles/rebreak-content-filter-sideload.mobileconfig";
@ -780,6 +822,15 @@ async function checkInlineEnrollment() {
} }
await refreshMdmStatus(); await refreshMdmStatus();
// After successful enrollment, automatically continue to lock profile if needed.
if (!localLock.value) {
enrollmentLogs.value.push("→ Enrollment abgeschlossen. Starte Lock-Profil …");
closeInlineEnrollment();
await startInlineLockProfile();
return;
}
enrollmentPhase.value = "success"; enrollmentPhase.value = "success";
} catch (e: any) { } catch (e: any) {
enrollmentError.value = e?.message ?? "Prüfung fehlgeschlagen"; enrollmentError.value = e?.message ?? "Prüfung fehlgeschlagen";
@ -797,29 +848,60 @@ function closeInlineEnrollment() {
} }
async function startInlineLockProfile() { async function startInlineLockProfile() {
if (!props.iphone?.udid) return;
lockPhase.value = "loading"; lockPhase.value = "loading";
lockError.value = null; lockError.value = null;
lockLogs.value = []; lockLogs.value = [];
lockQrUrl.value = "";
try { try {
lockLogs.value.push("→ Installiere Lock-Profil per MDM …"); lockLogs.value.push("→ Starte lokalen Server für Lock-Profil …");
const result = await mdmInstallLockProfile(props.iphone.udid, LOCK_PROFILE_PATH); const serverInfo = await startLocalProfileServer(LOCK_PROFILE_PATH);
lockLogs.value.push(`Command UUID: ${result.command_uuid}`); lockLogs.value.push(`Server gestartet: ${serverInfo.url}`);
lockQrUrl.value = await QRCode.toDataURL(serverInfo.qr_payload, {
width: 192,
margin: 2,
});
lockPhase.value = "waiting";
} catch (e: any) {
lockError.value = e?.message ?? "Lock-Profil-Server konnte nicht gestartet werden";
lockLogs.value.push(`${lockError.value}`);
lockPhase.value = "error";
}
}
async function checkInlineLockProfile() {
if (!props.iphone) return;
lockPhase.value = "checking";
lockError.value = null;
try {
const ids = await getInstalledProfiles();
props.iphone.installedProfileIDs = ids;
if (!ids.includes(LOCK_PROFILE_ID)) {
lockError.value = "Lock-Profil noch nicht installiert. Bitte QR-Code scannen und Profil installieren.";
lockPhase.value = "error";
return;
}
lockLogs.value.push("✓ Lock-Profil erkannt");
await refreshMdmStatus(); await refreshMdmStatus();
lockPhase.value = "success"; lockPhase.value = "success";
} catch (e: any) { } catch (e: any) {
lockError.value = e?.message ?? "Lock-Profil-Installation fehlgeschlagen"; lockError.value = e?.message ?? "Prüfung fehlgeschlagen";
lockLogs.value.push(`${lockError.value}`); lockLogs.value.push(`${lockError.value}`);
lockPhase.value = "error"; lockPhase.value = "error";
} }
} }
function closeInlineLockProfile() { function closeInlineLockProfile() {
stopLocalProfileServer();
lockPhase.value = "idle"; lockPhase.value = "idle";
lockError.value = null; lockError.value = null;
lockLogs.value = []; lockLogs.value = [];
lockQrUrl.value = "";
} }
</script> </script>

View File

@ -3,12 +3,9 @@ use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread; use std::thread;
use tiny_http::{Response, Server}; use tiny_http::{Response, Server};
static SERVER_RUNNING: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalServerInfo { pub struct LocalServerInfo {
pub url: String, pub url: String,
@ -17,10 +14,6 @@ pub struct LocalServerInfo {
#[tauri::command] #[tauri::command]
pub fn start_local_profile_server(profile_path: String) -> AppResult<LocalServerInfo> { pub fn start_local_profile_server(profile_path: String) -> AppResult<LocalServerInfo> {
if SERVER_RUNNING.load(Ordering::SeqCst) {
return Err(AppError::new("Local server is already running"));
}
let path = PathBuf::from(profile_path); let path = PathBuf::from(profile_path);
if !path.exists() { if !path.exists() {
return Err(AppError::new(format!( return Err(AppError::new(format!(
@ -48,8 +41,6 @@ pub fn start_local_profile_server(profile_path: String) -> AppResult<LocalServer
let qr_payload = url.clone(); let qr_payload = url.clone();
let profile_bytes = fs::read(&path)?; let profile_bytes = fs::read(&path)?;
SERVER_RUNNING.store(true, Ordering::SeqCst);
thread::spawn(move || { thread::spawn(move || {
for request in server.incoming_requests() { for request in server.incoming_requests() {
let response = Response::from_data(profile_bytes.clone()) let response = Response::from_data(profile_bytes.clone())
@ -62,7 +53,6 @@ pub fn start_local_profile_server(profile_path: String) -> AppResult<LocalServer
); );
let _ = request.respond(response); let _ = request.respond(response);
} }
SERVER_RUNNING.store(false, Ordering::SeqCst);
}); });
Ok(LocalServerInfo { url, qr_payload }) Ok(LocalServerInfo { url, qr_payload })
@ -71,8 +61,7 @@ pub fn start_local_profile_server(profile_path: String) -> AppResult<LocalServer
#[tauri::command] #[tauri::command]
pub fn stop_local_profile_server() -> AppResult<()> { pub fn stop_local_profile_server() -> AppResult<()> {
// tiny_http does not support graceful shutdown out of the box. // tiny_http does not support graceful shutdown out of the box.
// In a real implementation, store the server handle and close it. // Old server threads keep running, but each new profile gets a fresh port.
SERVER_RUNNING.store(false, Ordering::SeqCst);
Ok(()) Ok(())
} }