apps/rebreak-binder-mac/ — neue macOS-App die User durch den kompletten Self-Bind-Prozess führt: Welcome → Preflight → Supervise → Enroll → Configure (MDM-Push + Pre/Post-Check) → Sideload Lock-Profile (AirDrop). 3-Layer Smart-Resume: supervised? + Enrollment-Profil installed (cfgutil Ground-Truth)? + MDM-Ack fresh (NanoMDM-DB via ssh+psql)? Services: DeviceDetector (ideviceinfo + cfgutil), SuperviseRunner (spawnt supervise-magic CLI), MDMClient (PUT /v1/enqueue?push=1, Apple XML-Plist, identisch zum server-watcher-Format), MDMStatus (DB-Real- Check + ManagedApplicationList-Result-Read). Plus: - fix(supervise-magic): EOF nach ProcessMessage Response (ErrorCode=0) ist Success, nicht Error — vermeidet false-fail bei iPhone-Restore- Reboot - feat(mdm-profiles): rebreak-content-filter-mdm.mobileconfig als MDM-Push-Variante (ohne ConsentText, ohne globales allowAppRemoval= false — per-app via managed-state) End-to-End validiert: App-Push via Ad-Hoc-Manifest (silent), Managed- State via ManagedApplicationList-Query, NEFilter-Mode nach App-Force- Quit, Lock-Profile non-removable nach Sideload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
127 lines
4.6 KiB
Swift
127 lines
4.6 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
|
|
struct EnrollView: View {
|
|
@Environment(WizardModel.self) private var model
|
|
|
|
@State private var downloadStatus: String?
|
|
@State private var localPath: String?
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
header
|
|
|
|
Text("Jetzt installierst du das **minimale** MDM-Enrollment-Profile, damit dein iPhone mit unserem NanoMDM-Server (mdm.rebreak.org) sprechen kann. Das Profile bringt **keine Restrictions** — nur den MDM-Channel. Restrictions kommen später per Sideload-Lock.")
|
|
.foregroundStyle(.secondary)
|
|
|
|
instructions
|
|
|
|
Spacer()
|
|
|
|
navigationBar
|
|
}
|
|
.padding(40)
|
|
.onAppear { downloadProfile() }
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack {
|
|
Image(systemName: "doc.badge.gearshape")
|
|
.font(.system(size: 30))
|
|
.foregroundStyle(.tint)
|
|
Text("MDM-Enrollment")
|
|
.font(.title).bold()
|
|
}
|
|
}
|
|
|
|
private var instructions: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
stepRow(number: 1, text: "Profile wird automatisch vom Server runtergeladen.")
|
|
if let status = downloadStatus {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: localPath != nil ? "checkmark.circle.fill" : "arrow.down.circle")
|
|
.foregroundStyle(localPath != nil ? .green : .secondary)
|
|
Text(status).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
.padding(.leading, 36)
|
|
}
|
|
|
|
stepRow(number: 2, text: "Klick „Per AirDrop senden\" → wähle dein iPhone im Sheet.")
|
|
stepRow(number: 3, text: "Am iPhone: AirDrop-Dialog akzeptieren → Settings öffnet sich automatisch.")
|
|
stepRow(number: 4, text: "Settings → „Installieren\" tappen → 6-stelligen Geräte-Code eingeben → „Installieren\" bestätigen.")
|
|
stepRow(number: 5, text: "Zurück hier klick auf „Enrollment fertig → Weiter\".")
|
|
}
|
|
}
|
|
|
|
private func stepRow(number: Int, text: String) -> some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
ZStack {
|
|
Circle().fill(Color.accentColor)
|
|
Text("\(number)").foregroundStyle(.white).bold()
|
|
}
|
|
.frame(width: 24, height: 24)
|
|
Text(text)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
private var navigationBar: some View {
|
|
HStack {
|
|
Button("Zurück") { model.goTo(.supervise) }
|
|
.buttonStyle(.bordered)
|
|
Spacer()
|
|
if let path = localPath {
|
|
Button("Per AirDrop senden") {
|
|
sendViaAirDrop(path: path)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
Button("…im Finder zeigen") {
|
|
NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: path)])
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
Button("Enrollment fertig → Weiter") {
|
|
model.device?.isEnrolled = true
|
|
model.advance()
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
}
|
|
|
|
private func downloadProfile() {
|
|
let dest = "/tmp/rebreak-enrollment.mobileconfig"
|
|
downloadStatus = "Lade von mdm.rebreak.org …"
|
|
Task {
|
|
do {
|
|
guard let url = URL(string: "https://mdm.rebreak.org/enrollment/rebreak-enrollment.mobileconfig") else {
|
|
throw URLError(.badURL)
|
|
}
|
|
let (data, response) = try await URLSession.shared.data(from: url)
|
|
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
try data.write(to: URL(fileURLWithPath: dest))
|
|
await MainActor.run {
|
|
localPath = dest
|
|
downloadStatus = "Geladen: \(dest) (\(data.count) Bytes)"
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
downloadStatus = "Download fehlgeschlagen: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func sendViaAirDrop(path: String) {
|
|
let url = URL(fileURLWithPath: path)
|
|
guard let service = NSSharingService(named: .sendViaAirDrop), service.canPerform(withItems: [url]) else {
|
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
|
return
|
|
}
|
|
service.perform(withItems: [url])
|
|
}
|
|
}
|