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:
parent
5404f6676b
commit
92ab26605f
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user